Login to appengine from android client

2019-05-04 22:07发布

I am attempting to login to appengine to app engine and access the User service API in app engine. Basically I want to be able to see who is logged in to my servlets. I am using the authentication flow of getting authtoken from android and then gettting a ASID(or SACID) cookie from app engine. The cookie is then sent with the http request to the appengine servlet. This seems to work all fine, however when I attempt to get the user with this code:

UserService userService = UserServiceFactory.getUserService();
User user= userService.getCurrentUser();

user is always null. My question is am I missing something here? Why is the user service returning a null user? Below is my appengine and android code. Any help would be greatly appreciated!

App engine:

public class MyServlet extends HttpServlet {

public void process(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException {
 resp.setContentType("text/plain");

 UserService userService = UserServiceFactory.getUserService();
User user= userService.getCurrentUser();    
 }

public void doPost(HttpServletRequest req, HttpServletResponse resp)
 throws IOException, ServletException {
process(req, resp);
 }

public void doGet(HttpServletRequest req, HttpServletResponse resp)
 throws IOException, ServletException {
 process(req, resp);
 }
}

Android code:

public class AppEngineClient {
static final String BASE_URL = Util.getBaseUrl(this);
private static final String AUTH_URL = BASE_URL + "/_ah/login";
private static final String AUTH_TOKEN_TYPE = "ah";

private final Context mContext;
private final String mAccountName;

private static final String TAG = "AppEngineClient";

public AppEngineClient(Context context, String accountName) {
    this.mContext = context;
    this.mAccountName = accountName;
}

public HttpResponse makeRequest(String urlPath, List<NameValuePair> params) throws Exception {
    HttpResponse res = makeRequestNoRetry(urlPath, params, false);
    if (res.getStatusLine().getStatusCode() == 500) {
        res = makeRequestNoRetry(urlPath, params, true);
    }
    return res;
}

private HttpResponse makeRequestNoRetry(String urlPath, List<NameValuePair> params, boolean newToken)
        throws Exception {
    // Get auth token for account
    Account account = new Account(mAccountName, "com.google");
    String authToken = getAuthToken(mContext, account);

    if (newToken) {  // invalidate the cached token
        AccountManager accountManager = AccountManager.get(mContext);
        accountManager.invalidateAuthToken(account.type, authToken);
        authToken = getAuthToken(mContext, account);
    }

    // Get SACSID cookie
    DefaultHttpClient client = new DefaultHttpClient();
    String continueURL = BASE_URL;
    URI uri = new URI(AUTH_URL + "?continue=" +
            URLEncoder.encode(continueURL, "UTF-8") +
            "&auth=" + authToken);
    HttpGet method = new HttpGet(uri);
    final HttpParams getParams = new BasicHttpParams();
    HttpClientParams.setRedirecting(getParams, false);  // continue is not used
    method.setParams(getParams);

    HttpResponse res = client.execute(method);
    Header[] headers = res.getHeaders("Set-Cookie");
    if (res.getStatusLine().getStatusCode() != 302 ||
            headers.length == 0) {
        return res;
    }

    String sascidCookie = null;
    for (Header header: headers) {
        if (header.getValue().indexOf("SACSID=") >=0) {
            // let's parse it
            String value = header.getValue();
            String[] pairs = value.split(";");
            ascidCookie = pairs[0];
        }
    }

    // Make POST request
    uri = new URI(BASE_URL + urlPath);
    HttpPost post = new HttpPost(uri);
    UrlEncodedFormEntity entity =
        new UrlEncodedFormEntity(params, "UTF-8");
    post.setEntity(entity);
    post.setHeader("Cookie", ascidCookie);
    post.setHeader("X-Same-Domain", "1");  // XSRF
    res = client.execute(post);
    return res;
}

private String getAuthToken(Context context, Account account) throws PendingAuthException {
    String authToken = null;
    AccountManager accountManager = AccountManager.get(context);
    try {
        AccountManagerFuture<Bundle> future =
                accountManager.getAuthToken (account, AUTH_TOKEN_TYPE, false, null, null);
        Bundle bundle = future.getResult();
        authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
        if (authToken == null) {
            throw new PendingAuthException(bundle);
        }
    } catch (OperationCanceledException e) {
        Log.w(TAG, e.getMessage());
    } catch (AuthenticatorException e) {
        Log.w(TAG, e.getMessage());
    } catch (IOException e) {
        Log.w(TAG, e.getMessage());
    }
    return authToken;
}

public class PendingAuthException extends Exception {
    private static final long serialVersionUID = 1L;
    private final Bundle mAccountManagerBundle;
    public PendingAuthException(Bundle accountManagerBundle) {
        super();
        mAccountManagerBundle = accountManagerBundle;
    }

    public Bundle getAccountManagerBundle() {
        return mAccountManagerBundle;
    }
}

}

2条回答
Deceive 欺骗
2楼-- · 2019-05-04 22:20

The Android code above is getting a ClientLogin token from the Google Accounts API. For login and getting the current user via UserService, the GAE app must be using Google Accounts API for authentication as well ('Application settings'->'Authentication options').

查看更多
Fickle 薄情
3楼-- · 2019-05-04 22:32

I noticed that it is very easy to add authentication to existing AppEngine endpoint - you just need to add com.google.appengine.api.users.User parameter to your method. And finally I found what is going under the hood and how to authenticate in the same way arbitrary servlet. So to authenticate on Android side you need: 1) choose account:

private void signIn()
{
    startActivityForResult(GoogleAccountCredential.usingAudience(this, "server:client_id:{id}.apps.googleusercontent.com").newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode)
    {
    case REQUEST_ACCOUNT_PICKER:
        if (data != null && data.getExtras() != null)
        {
            String accountName = data.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME);
            if (accountName != null)
            {
                // TODO save accountName
            }
        }
        break;
    }
}

2) get Credential object:

GoogleAccountCredential credential = GoogleAccountCredential.usingAudience(this, "server:client_id:{id}.apps.googleusercontent.com");
credential.setSelectedAccountName(accountName);

3) create Google HttpRequest object and make the request:

HttpTransport transport = new NetHttpTransport();
HttpRequestFactory requestFactory = transport.createRequestFactory(credential);
GenericUrl url = new GenericUrl(UPLOAD_SERVICE_URL);

HttpRequest request = requestFactory.buildGetRequest(url);
HttpResponse resp = request.execute();
// TODO check response

To authenticate the request on AppEngine side you can use internal class WebApisUserService declared in "appengine-endpoints.jar" library. This is just the class used by AppEngine internally in endpoints. Unfortunately this class constructor and other required methods are protected from external usage so we need to use the reflection to access it. Complete helper class is the following:

public class WebApisUserServiceHelper
{
    public static WebApisUserService createInstance(boolean isClientIdWhitelistEnabled) 
            throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException
    {
        Constructor<WebApisUserService> constructor;
        constructor = WebApisUserService.class.getDeclaredConstructor(Boolean.TYPE);
        constructor.setAccessible(true);
        WebApisUserService ret = constructor.newInstance(isClientIdWhitelistEnabled);
        return ret;
    }

    public static User getCurrentUser(WebApisUserService service, HttpServletRequest req, String appName, List<String> audiences, List<String> clientIds) 
            throws NoSuchMethodException, SecurityException, ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
    {
        String token = getAuthToken(service, req);
        if (token != null)
        {
            List<String> allowedScopes = new ArrayList<String>();
            allowedScopes.add("https://www.googleapis.com/auth/userinfo.email");

            return getCurrentUser(service, token, allowedScopes, audiences, clientIds);
    }

        return null;
    }

    private static User getCurrentUser(WebApisUserService service, String token, List<String> allowedScopes, List<String> allowedAudiences, List<String> allowedClientIds) 
            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
    {
        Method method = WebApisUserService.class.getDeclaredMethod("getCurrentUser", String.class, List.class, List.class, List.class);
        method.setAccessible(true);
        Object ret = method.invoke(service, token,  allowedScopes, allowedAudiences, allowedClientIds);
        if (ret instanceof User) return (User) ret;
        return null;
    }

    private static String getAuthToken(WebApisUserService service, HttpServletRequest request) 
            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
    {
        Method method = WebApisUserService.class.getDeclaredMethod("getAuthToken", HttpServletRequest.class);
        method.setAccessible(true);
        Object ret = method.invoke(service, request);
        if (ret instanceof String) return (String) ret;
        return null;
    }
}

and here is how to use this helper:

public class MyServlet extends HttpServlet
{
    private final WebApisUserService auth = createAuthService();

    private static WebApisUserService createAuthService()
    {
        try
        {
            return WebApisUserServiceHelper.createInstance(false);
        }
        catch (Exception e)
        {
            log.log(Level.WARNING, "Failed to create WebApisUserServiceFactory instance. Exception: %s", e.toString());
        }
        return null;
    }

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
    {
        try
        {
            User user = authenticateUserSafe(req);
            if (user == null)
            {
                resp.sendError(401, "auth required");
                return;
            }

            String str = String.format("User id: %s, nick: %s, email: %s", user.getUserId(), user.getNickname(), user.getEmail());
            resp.getWriter().write(str);
        }
        catch (Throwable e)
        {
            resp.getWriter().write("Exception: " + e);
        }
    }

    private User authenticateUserSafe(HttpServletRequest req)
    {
        try
        {
            return authenticateUser(req);
        }
        catch (Exception e)
        {
            log.log(Level.WARNING, "Failed to authenticate user. Exception: %s", e.toString());
        }
        return null;
    }

    private User authenticateUser(HttpServletRequest req) 
            throws NoSuchMethodException, SecurityException, ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
    {
        List<String> audiences = new ArrayList<String>();
        audiences.add(Ids.ANDROID_AUDIENCE);

        List<String> clientIds = new ArrayList<String>();
        clientIds.add(Ids.WEB_CLIENT_ID);
        clientIds.add(Ids.ANDROID_CLIENT_ID);

        return WebApisUserServiceHelper.getCurrentUser(auth, req, "{id}", audiences, clientIds);
    }
}

This approach checked with AppEngine 1.8.6. I hope Google will open WebApisUserService class publicly so reflection won't be required.

查看更多
登录 后发表回答