简体   繁体   中英

UserService.getCurrentUser() returns null on Google App Engine

I have a use case that looks like this:

  1. User enters https://mydomain.appspot.com/listen
  2. User is redirected to Google for Authentication
  3. If success, application sends http request to Google to enable push notifications for changes in a specific file(Sheet) on Google Drive
  4. User enters Google Sheets and edits the file.
  5. Google sends a http post to my application( https://mydomain.appspot.com/notifications ) with file id and some other data.
  6. My application receives the http post, verifies file id and tries to open the file to see the content.

Step 6 doesn't work. I get a NullPointerException on the second line when doing this:

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

I don't really know how I should solve this. In step 1-3 the user logs in and grants access to the file. Step 5-6 is triggered from Google. If it was triggered from the user, then the user could be redirected to a login page. That is not an option since the request is coming from Google.

Is there any way to make this work? Note: The file in question belongs to a specific user. It is not owned by some kind of service account.

I have based my Sheet authentication on the sample provided by Google. Looks something like this:

public class ConcreteSheetWriter implements SheetWriter {


    public ConcreteSheetWriter(DriveFileMaker driveFileMaker) {
        DriveFileMaker driveFileMaker1 = driveFileMaker;

        try {
            httpTransport = GoogleNetHttpTransport.newTrustedTransport();
            dataStoreFactory = AppEngineDataStoreFactory.getDefaultInstance(); //TODO replace with appenginedatastore otherwise restart is painful
        } catch (Throwable t) {
            t.printStackTrace();
            //   System.exit(1); TODO potentially fix for app engine
            logger.warning("Could not connect to sheets");
            throw new RuntimeException(t);
        }


    }

    private static Credential authorize(HttpTransport HTTP_TRANSPORT, DataStoreFactory dataStoreFactory) throws IOException {
        // Load client secrets.
        InputStream in =
                ConcreteSheetWriter.class.getResourceAsStream(SECRET_PATH);
        GoogleClientSecrets clientSecrets =
                GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));
        /* THE CODE BELOW IN THIS METHOD REPRESENT STEP 6 */
        // Build flow and trigger user authorization request.
        GoogleAuthorizationCodeFlow flow =
                new GoogleAuthorizationCodeFlow.Builder(
                        HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
                        .setDataStoreFactory(dataStoreFactory)
                        .setAccessType("offline")
                        .build();
       /*

       The credentials before deploying to GAE. Problems when deploying on GAE
       Credential credential = new AuthorizationCodeInstalledApp(
                flow, new LocalServerReceiver()).authorize("user");
        */
        final UserService userService = UserServiceFactory.getUserService();
        final User user = userService.getCurrentUser();
        logger.info("User is " + user);
        final String userId = user.getUserId();
        final Credential credential = flow.loadCredential(userId);
        return credential;
    }

    @Override
    public List<List<String>> read(String changedFileId) {
        Sheets service = null;
        final String range = "Sheet1!A1:AF30";
        try {
            service = getSheetsService(authorize(httpTransport, dataStoreFactory), httpTransport);
            ValueRange spreadsheets = service.spreadsheets().values().get(changedFileId, range).execute();
            return convert(spreadsheets.getValues());
        } catch (IOException e) {
            throw new CouldNotCommunicateWithGoogleSheetsException(e);
        }


    }
}

Here is the code for logging the user in, represents step 1-3:

public class PlusSampleServlet extends AbstractAppEngineAuthorizationCodeServlet {
    private final static Logger logger = Logger.getLogger(PlusSampleServlet.class.getName());
    private static final long serialVersionUID = 1L;

    private final DriveUtilityService driveUtilityService;


    public PlusSampleServlet() {
        //omitted
    }

    private static void addLoginLogoutButtons(HttpServletRequest req, HttpServletResponse resp, StringBuilder resultFromWatch, UserService userService, String thisUrl, PrintWriter respWriter) throws IOException {

        //omitted
    }

    private static Optional<Channel> watchFile(Drive service, String fileId,
                                               String channelId, String channelType, String channelAddress) throws IOException {
        final Channel returnValue;
        final Channel channel = new Channel();
        channel.setId(channelId);
        channel.setType(channelType);
        channel.setAddress(channelAddress);
        final Drive.Files tmp = service.files();
        returnValue = tmp.watch(fileId, channel).execute();
        return Optional.fromNullable(returnValue);
    }

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {

        AuthorizationCodeFlow authFlow = initializeFlow();
        final String userId = getUserId(req);
        Credential credential = authFlow.loadCredential(userId);
        logger.info("Executing listener activation for user " + userId);
        StringBuilder resultFromWatch = new StringBuilder();
        Drive drive = new Drive.Builder(Utils.HTTP_TRANSPORT, Utils.JSON_FACTORY, credential).setApplicationName("t").build();

        try {

            Optional<Channel> channel = watchFile(drive, driveUtilityService.getFileId(), driveUtilityService.getChannelId(), "web_hook", driveUtilityService.getPushUrl());
            String channelStringTmp;
            if (channel.isPresent()) {
                channelStringTmp = channel.get().toString();
            } else {
                channelStringTmp = "null...";
            }
            resultFromWatch.append(channelStringTmp);
        } catch (Exception e) {
            resultFromWatch.append(e.getMessage());
        }

        final UserService userService = UserServiceFactory.getUserService();
        final String thisUrl = req.getRequestURI();
        // Send the results as the response
        PrintWriter respWriter = resp.getWriter();
        resp.setStatus(200);
        resp.setContentType("text/html");

        addLoginLogoutButtons(req, resp, resultFromWatch, userService, thisUrl, respWriter);

        logger.warning("user is " + userId + " sample has done its job and channel " + resultFromWatch.toString());
    }

    @Override
    protected AuthorizationCodeFlow initializeFlow() throws ServletException, IOException {
        return Utils.initializeFlow();
    }

    @Override
    protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
        return Utils.getRedirectUri(req);
    }
}

The utils class:

class Utils {
    static final String MAIN_SERVLET_PATH = "/plussampleservlet";
    static final String AUTH_CALLBACK_SERVLET_PATH = "/oauth2callback";
    static final UrlFetchTransport HTTP_TRANSPORT = new UrlFetchTransport();
    static final JacksonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
    private final static Logger logger = Logger.getLogger(Utils.class.getName());
    /**
     * Global instance of the {@link DataStoreFactory}. The best practice is to make it a single
     * globally shared instance across your application.
     */
    private static final AppEngineDataStoreFactory DATA_STORE_FACTORY =
            AppEngineDataStoreFactory.getDefaultInstance();
    private static final Set<String> SCOPES = getScopes();
    private static GoogleClientSecrets clientSecrets = null;


    private static Set<String> getScopes() {
        List<String> scopeList = Arrays.asList(DriveScopes.DRIVE_READONLY, SheetsScopes.SPREADSHEETS_READONLY);
        Set<String> scopes = Sets.newHashSet();
        scopes.addAll(scopeList);
        return scopes;
    }

    private static GoogleClientSecrets getClientSecrets() throws IOException {
        if (clientSecrets == null) {
            clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
                    new InputStreamReader(Utils.class.getResourceAsStream("/plus_secret.json")));
            Preconditions.checkArgument(!clientSecrets.getDetails().getClientId().startsWith("Enter ")
                            && !clientSecrets.getDetails().getClientSecret().startsWith("Enter "),
                    "Download client_secrets.json file from https://code.google.com/apis/console/?api=plus "
                            + "into plus-appengine-sample/src/main/resources/client_secrets.json");
        }
        logger.info("Something asked for the secret");
        return clientSecrets;
    }

    static GoogleAuthorizationCodeFlow initializeFlow() throws IOException {
        logger.info("flow is initialized soon");
        return new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT, JSON_FACTORY, getClientSecrets(), SCOPES).setDataStoreFactory(
                DATA_STORE_FACTORY).setAccessType("offline").build();
    }

    static String getRedirectUri(HttpServletRequest req) {
        GenericUrl requestUrl = new GenericUrl(req.getRequestURL().toString());
        requestUrl.setRawPath(AUTH_CALLBACK_SERVLET_PATH);
        logger.info("retrieved redirecturl");
        return requestUrl.build();
    }
}

The callback when "login" is done:

public class PlusSampleAuthCallbackServlet
        extends AbstractAppEngineAuthorizationCodeCallbackServlet {
    private final static Logger logger = Logger.getLogger(PlusSampleAuthCallbackServlet.class.getName());

    private static final long serialVersionUID = 1L;

    @Override
    protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
            throws ServletException, IOException {
        resp.sendRedirect(Utils.MAIN_SERVLET_PATH);
        logger.info("ON success");
    }

    @Override
    protected void onError(
            HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
            throws ServletException, IOException {
        String nickname = UserServiceFactory.getUserService().getCurrentUser().getNickname();
        resp.getWriter().print("<h3>Hey " + nickname + ", why don't you want to play with me?</h1>");
        resp.setStatus(200);
        resp.addHeader("Content-Type", "text/html");
        logger.info("ON error");
        return;
    }

    @Override
    protected AuthorizationCodeFlow initializeFlow() throws ServletException, IOException {
        logger.info("initializing flow");
        return Utils.initializeFlow();
    }

    @Override
    protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
        logger.info("get redirect");
        return Utils.getRedirectUri(req);
    }

}

At step 3 you would need to save (in the datastore, for example) the mapping information tying together the notification registration, the doc and the user (there must be some context info in the registration /notification as the same user can watch multiple docs and multiple users can watch the same doc)

At step 6 the app retrieves the saved mapping info based on the notification (the post request) context and can then identify the user credentials that needs to be used when attempting to open the file.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM