在没有Google帐户登录的情况下向Cloud Endpoints验证您的客户端

时间:2015-08-20 19:43:35

标签: android google-app-engine authentication google-oauth google-cloud-endpoints

我一直在研究如何使用Cloud Endpoints 验证您的客户端(Android,iOS,网络应用),而要求您的用户使用他们的Google帐户登录{{3告诉你。

这样做的原因是我希望保护我的API或者#34;将其锁定"仅限我指定的客户。有时我会有一个没有用户登录的应用程序。我讨厌纠缠我的用户现在登录,因此我的API是安全的。或者其他时候,我只想在网站上管理自己的用户,而不是使用Google+,Facebook或其他任何登录身份验证。

首先,让我首先展示使用您的Cloud Endpoints API使用documentation中指定的Google帐户登录信息验证Android应用的方式。在那之后,我将向您展示我的发现以及我需要帮助的解决方案的潜在领域。

(1)指定授权向API后端发出请求的应用程序的客户端ID(clientIds);(2)将User参数添加到要通过授权保护的所有公开方法。

public class Constants {
      public static final String WEB_CLIENT_ID = "1-web-apps.apps.googleusercontent.com";
      public static final String ANDROID_CLIENT_ID = "2-android-apps.googleusercontent.com";
      public static final String IOS_CLIENT_ID = "3-ios-apps.googleusercontent.com";
      public static final String ANDROID_AUDIENCE = WEB_CLIENT_ID;

      public static final String EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.email";
    }


import com.google.api.server.spi.auth.common.User; //import for the User object

    @Api(name = "myApi", version = "v1",
         namespace = @ApiNamespace(ownerDomain = "${endpointOwnerDomain}",
         ownerName = "${endpointOwnerDomain}",
         packagePath="${endpointPackagePath}"),
         scopes = {Constants.EMAIL_SCOPE}, 
         clientIds = {Constants.WEB_CLIENT_ID, Constants.ANDROID_CLIENT_ID,
                      Constants.IOS_CLIENT_ID,
                      Constants.API_EXPLORER_CLIENT_ID},
                      audiences = {Constants.ANDROID_AUDIENCE})

    public class MyEndpoint {

        /** A simple endpoint method that takes a name and says Hi back */
        @ApiMethod(name = "sayHi")
        public MyBean sayHi(@Named("name") String name, User user) throws UnauthorizedException {
            if (user == null) throw new UnauthorizedException("User is Not Valid");
            MyBean response = new MyBean();
            response.setData("Hi, " + name);

            return response;
        }

    } 

(3)在Android中调用Asynctask中的API方法,确保传递credential中的Builder变量:

class EndpointsAsyncTask extends AsyncTask<Pair<Context, String>, Void, String> {
        private static MyApi myApiService = null;
        private Context context;

        @Override
        protected String doInBackground(Pair<Context, String>... params) {
            credential = GoogleAccountCredential.usingAudience(this,
            "server:client_id:1-web-app.apps.googleusercontent.com");
            credential.setSelectedAccountName(settings.getString(PREF_ACCOUNT_NAME, null));
            if(myApiService == null) {  // Only do this once
                MyApi.Builder builder = new MyApi.Builder(AndroidHttp.newCompatibleTransport(),
                        new AndroidJsonFactory(), credential)
                    // options for running against local devappserver
                    // - 10.0.2.2 is localhost's IP address in Android emulator
                    // - turn off compression when running against local devappserver
                    .setRootUrl("http://<your-app-engine-project-id-here>/_ah/api/")
                    .setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {
                        @Override
                        public void initialize(AbstractGoogleClientRequest<?> abstractGoogleClientRequest) throws IOException {
                            abstractGoogleClientRequest.setDisableGZipContent(true);
                        }
                    });
                    // end options for devappserver

                myApiService = builder.build();
            }

            context = params[0].first;
            String name = params[0].second;

            try {
                return myApiService.sayHi(name).execute().getData();
            } catch (IOException e) {
                return e.getMessage();
            }
        }

        @Override
        protected void onPostExecute(String result) {
            Toast.makeText(context, result, Toast.LENGTH_LONG).show();
        }
    }

在您的Android应用中,您首先要显示Google帐户选择器,将Google帐户电子邮件存储在您的共享首选项中,然后将其设置为GoogleAccountCredential对象的一部分(更多信息,请参阅怎么做documentation)。

Google App Engine服务器会收到您的请求并进行检查。如果Android客户端是您在@Api表示法中指定的客户端之一,则服务器会将com.google.api.server.spi.auth.common.User对象注入您的API方法。您现在有责任在API方法中检查User对象是否为null。如果User对象为null,则应在方法中引发异常以防止其运行。如果您不进行此项检查,则会执行您的API方法(如果您尝试限制对其的访问权限,则禁止执行)。

您可以转到Google Developers Console来获取ANDROID_CLIENT_ID。在那里,您提供Android应用程序的包名称和SHA1,它为您生成一个Android客户端ID,供您在@Api注释中使用(或将其放在上面指定的类Constants中可用性)。

我已经对上述所有内容进行了一些广泛的测试,这是我发现的:

如果您在@Api注释中指定了虚假或无效的Android clientId,则{API}方法中的User对象将为null。如果您正在检查if (user == null) throw new UnauthorizedException("User is Not Valid");,那么您的API方法将无法运行。

这是令人惊讶的,因为看起来在Cloud Endpoints中有一些幕后验证会检查Android ClientId是否有效。如果它无效,它将无法返回User对象 - 即使最终用户登录到其Google帐户且GoogleAccountCredential有效。

我的问题是,是否有人知道如何在我的Cloud Endpoints方法中自行检查该类型的ClientId验证?例如,可以在HttpHeader中传递这些信息吗?

Cloud Endpoints中的另一个注入类型是javax.servlet.http.HttpServletRequest。您可以在API方法中获得这样的请求:

@ApiMethod(name = "sayHi")
            public MyBean sayHi(@Named("name") String name, HttpServletRequest req) throws UnauthorizedException {

                String Auth = req.getHeader("Authorization");//always null based on my tests
                MyBean response = new MyBean();
                response.setData("Hi, " + name);

                return response;
            }

        }  

但我不确定是否有必要的信息或如何获得它。

当然某处必须有一些数据告诉我们客户是@Api clientIds中的授权和指定客户。

这样,您可以将API锁定到Android应用程序(以及可能的其他客户端),而无需纠缠最终用户登录(或只创建自己的简单用户名+密码登录)。

尽管如此,你必须在null的第三个参数中传递Builder,如下所示:

MyApi.Builder builder = new MyApi.Builder(AndroidHttp.newCompatibleTransport(),                             新的AndroidJsonFactory(),null)

然后在您的API方法中提取调用是否来自经过身份验证的客户端,并抛出异常或运行您想要的任何代码。

我知道这是可能的,因为在GoogleAccountCredential中使用Builder时,云端点会以某种方式知道调用是否来自经过身份验证的客户端,然后注入其User对象基于此进入API方法与否。

这些信息可能会以某种方式出现在标题或正文中吗?如果是这样,我怎样才能把它拿出去检查我的API方法中是否存在?

注意:我阅读了有关此主题的其他帖子。它们提供了传递您自己的身份验证令牌的方法 - 这很好 - 但如果有人反编译您的.apk仍然不安全。我认为如果我的假设有效,您将能够在没有任何登录的情况下将您的Cloud Endpoints API锁定到客户端。

here

Custom Authentication for Google Cloud Endpoints (instead of OAuth2)

Authenticate my "app" to Google Cloud Endpoints not a "user"

修改 我们使用了Google云平台的金牌支持,并与他们的支持团队进行了数周的讨论。这是他们的最终答案:

  

&#34;不幸的是,我对此没有任何好运。我问过我的问题   团队,并检查了所有文档。它看起来像使用OAuth2   是你唯一的选择。原因是端点服务器处理   验证到达您的应用程序之前。这意味着你不会   能够开发自己的身份验证流程,并获得结果   就像你用令牌看到的一样。

     

我很乐意为您提交功能请求。如果你可以   提供有关OAuth2流无法解释的原因的更多信息   为您的客户工作,我可以把剩下的信息   一起并将其提交给产品经理。&#34;

(皱眉的脸) - 然而,也许它还有可能吗?

3 个答案:

答案 0 :(得分:1)

我已经使用自定义标头实现了Endpoint Auth&#34;授权&#34;它工作得很好。在我的情况下,此令牌在登录后设置,但应该与您的应用程序完全相同。检查您的测试,因为值应该在那里。 确实检索该标头的方法是:

String Auth = req.getHeader("Authorization");

您可以更进一步,定义自己的Authenticator实现,并将其应用于您的安全API调用。

答案 1 :(得分:1)

遇到同样的问题,找到一个解决方案,从我的终端安全地调用我的API,不使用Google帐户。我们无法反编译IOS App(Bundle),但反编译Android App非常简单..

我找到的解决方案并不完美,但做得很好:

  1. 在Android APP上,我只是简单地创建一个名为APIKey的常量String变量(例如“helloworld145698”)
  2. 然后我用sha1,下一个md5加密它,最后加载sha1(加密的顺序和频率)并将变量存储在 私有模式 <的SharedPref(For Android)中/ strong>(对您应用中的随机类执行此操作)这是我在我的后端加密的授权的结果!
  3. 在我的后端,我只是在每个请求
  4. 上添加一个参数(例如,名为token for token)

    <强> 实施例

     @ApiMethod(name = "sayHi")
        public void sayHi(@Named("name") String name, @Named("Token") String token) {
    
        if (token == tokenStoreOnAPIServer) {
             //Allow it
        } else {
             //Refuse it and print error
        } 
    
    }
    
    1. 在Android上,有效的ProGuard模糊了您的代码。对于那些试图反编译你的应用程序的人来说,这将是不可读的(逆向工程真的是硬核)
    2. 不是完美的安全解决方案,但它确实有用,并且真正(真的)很难找到真正的API密钥,以便任何试图在反编译后尝试读取代码的人。

答案 2 :(得分:0)

因此,您没有任何特定于用户的信息,但只是想确保只有您的应用能够与您的后端通信... 这就是我的想法,

变化

@Api(name = "myApi", version = "v1",
         namespace = @ApiNamespace(ownerDomain = "${endpointOwnerDomain}",
         ownerName = "${endpointOwnerDomain}",
         packagePath="${endpointPackagePath}"),
         scopes = {Constants.EMAIL_SCOPE}, 
         clientIds = {Constants.WEB_CLIENT_ID, Constants.ANDROID_CLIENT_ID,
                      Constants.IOS_CLIENT_ID,
                      Constants.API_EXPLORER_CLIENT_ID},
                      audiences = {Constants.ANDROID_AUDIENCE})
{
...
}

@Api(name = "myApi", version = "v1",
         namespace = @ApiNamespace(ownerDomain = "${endpointOwnerDomain}",
         ownerName = "${endpointOwnerDomain}",
         packagePath="${endpointPackagePath}"),
         scopes = {Constants.EMAIL_SCOPE}, 
         clientIds = {Constants.ANDROID_CLIENT_ID},
         audiences = {Constants.ANDROID_AUDIENCE})

{
...
}

客户端ID是根据您应用的签名生成的。它无法复制。如果您只允许端点接受来自Android应用程序的请求,那么您的问题就会得到解决。

告诉我这是否有效。