OAuth 2.0 וספריית לקוח OAuth של Google עבור Java

סקירה

המטרה: המסמך הזה מתאר את הפונקציות הכלליות של OAuth 2.0 שמוצעות על ידי ספריית הלקוח של Google OAuth ל-Java. אפשר להשתמש בפונקציות האלה לאימות ולהרשאה של כל שירותי האינטרנט.

במאמר שימוש ב-OAuth 2.0 עם ספריית הלקוח של Google API ל-Java מוסבר איך להשתמש ב-GoogleCredential להרשאות OAuth 2.0 בשירותי Google.

סיכום: OAuth 2.0 הוא מפרט סטנדרטי שמאפשר למשתמשי קצה לאשר באופן מאובטח לאפליקציית לקוח לגשת למשאבים מוגנים בצד השרת. נוסף על כך, במפרט אסימון למוכ"ז של OAuth 2.0 מוסבר איך לגשת למשאבים המוגנים האלה באמצעות אסימון גישה שהוענק בתהליך ההרשאה של משתמש הקצה.

לפרטים, אפשר לעיין במסמכי Javadoc של החבילות הבאות:

הרשמת לקוחות

לפני שתשתמשו בספריית הלקוח של Google OAuth ל-Java, סביר להניח שתצטרכו לרשום את האפליקציה בשרת הרשאות כדי לקבל מזהה לקוח וסוד לקוח. (למידע כללי על התהליך הזה, קראו את מפרט רישום הלקוחות).

מאגר פרטי הכניסה והפרטים

Credential הוא מחלקת עזר של OAuth 2.0 לגישה למשאבים מוגנים באמצעות אסימון גישה, והוא בטוח לשימוש בשרשורים. כשמשתמשים באסימון רענון, Credential מרענן את אסימון הגישה גם כשפג התוקף של אסימון הגישה באמצעות אסימון הרענון. לדוגמה, אם כבר יש לכם אסימון גישה, תוכלו לשלוח בקשה בדרכים הבאות:

  public static HttpResponse executeGet(
      HttpTransport transport, JsonFactory jsonFactory, String accessToken, GenericUrl url)
      throws IOException {
    Credential credential =
        new Credential(BearerToken.authorizationHeaderAccessMethod()).setAccessToken(accessToken);
    HttpRequestFactory requestFactory = transport.createRequestFactory(credential);
    return requestFactory.buildGetRequest(url).execute();
  }

רוב האפליקציות צריכות לשמור את אסימון הגישה של פרטי הכניסה ולרענן את אסימון הגישה, כדי להימנע מהפניות אוטומטיות לדף ההרשאות בדפדפן. ההטמעה של CredentialStore בספרייה הזו הוצאה משימוש ותוסר בגרסאות עתידיות. החלופה היא להשתמש בממשקים של DataStoreFactory ו-DataStore עם StoredCredential, שמסופק על ידי Google HTTP Client Library ל-Java.

אפשר להשתמש באחת מההטמעות הבאות שהספרייה מספקת:

  • פרטי הכניסה נשמרים ב-JdoDataStoreFactory באמצעות JDO.
  • פרטי הכניסה נשמרים ב-AppEngineDataStoreFactory באמצעות ממשק ה-API של Google App Engine Data Store.
  • פרטי הכניסה נשמרים בזיכרון ב-MemoryDataStoreFactory. הם נשמרים בזיכרון, מכיוון שהם משמשים רק כאחסון לטווח קצר לכל משך החיים של התהליך.
  • FileDataStoreFactory שומר את פרטי הכניסה בקובץ.

משתמשי Google App Engine:

AppEngineCredentialStore הוצא משימוש ו שאנחנו מסירים אותו.

אנחנו ממליצים להשתמש ב-AppEngineDataStoreFactory עם StoredCredential. אם יש לכם פרטי כניסה שאוחסנו בעבר, אתם יכולים להשתמש בשיטות העזר שנוספו מעבר ל-migrateTo(AppEngineDataStoreFactory) או migrateTo(DataStore) כדי לבצע את ההעברה.

משתמשים ב-DataStoreCredentialRefreshListener ומגדירים אותו עבור פרטי הכניסה באמצעות GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener).

תהליך קוד ההרשאה

משתמשים בתהליך קוד ההרשאה כדי לאפשר למשתמש הקצה להעניק לאפליקציה שלכם גישה לנתונים המוגנים שלו. הפרוטוקול לתהליך הזה מוגדר במפרט של הרשאות הקוד למתן הרשאות.

ההטמעה של התהליך הזה מתבצעת באמצעות AuthorizationCodeFlow. השלבים:

  • משתמש קצה מתחבר לאפליקציה. צריך לשייך את המשתמש הזה למזהה משתמש ייחודי לאפליקציה שלכם.
  • מפעילים את השיטה AuthorizationCodeFlow.loadCredential(String), על סמך מזהה המשתמש, כדי לבדוק אם פרטי הכניסה של המשתמש כבר ידועים. אם כן, סיימת.
  • אם לא, צריך לקרוא לפונקציה AuthorizationCodeFlow.newAuthorizationUrl() ולהפנות את הדפדפן של משתמש הקצה לדף הרשאה שבו הוא יוכל לתת לאפליקציה שלכם גישה לנתונים המוגנים שלו.
  • לאחר מכן דפדפן האינטרנט מפנה לכתובת ה-URL עם פרמטר השאילתה "code", שאפשר להשתמש בו כדי לבקש אסימון גישה באמצעות AuthorizationCodeFlow.newTokenRequest(String).
  • משתמשים ב-AuthorizationCodeFlow.createAndStoreCredential(TokenResponse, String) כדי לאחסן ולקבל פרטי כניסה כדי לגשת למשאבים מוגנים.

לחלופין, אם אתם לא משתמשים ב-AuthorizationCodeFlow, אתם יכולים להשתמש במחלקות ברמה נמוכה יותר:

זרימת קוד הרשאת Servlet

הספרייה הזו מספקת מחלקות מסייעות ל-servlet, כדי לפשט באופן משמעותי את תהליך קוד ההרשאה בתרחישים בסיסיים לדוגמה. כל מה שצריך לעשות הוא לספק מחלקות משנה ספציפיות של AbstractAuthorizationCodeServlet ו-AbstractAuthorizationCodeCallbackServlet (מ-google-oauth-client-servlet) ולהוסיף אותן לקובץ ה-web.xml. שימו לב שעדיין צריך לטפל בכניסת המשתמש לאפליקציית האינטרנט ולחלץ מזהה משתמש.

קוד לדוגמה:

public class ServletSample extends AbstractAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

public class ServletCallbackSample extends AbstractAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

זרימת קוד הרשאה של Google App Engine

תהליך קוד ההרשאה ב-App Engine כמעט זהה לתהליך של קוד ההרשאה של servlet, אבל אנחנו יכולים להשתמש ב-Users Java API של Google App Engine. כדי להפעיל את User Java API המשתמשים צריכים להתחבר. למידע על הפניית המשתמשים לדף התחברות (אם הם עדיין לא מחוברים), קראו את הקטע אבטחה ואימות (ב-web.xml).

ההבדל העיקרי לעומת התרחיש של servlet הוא שאתם מספקים מחלקות משנה קונקרטיות של AbstractAppEngineAuthorizationCodeServlet ושל AbstractAppEngineAuthorizationCodeCallbackServlet (מ-google-oauth-client-appengine). הם מרחיבים את מחלקות ה-servlet המופשטות ומטמיעים את השיטה getUserId עבורך באמצעות Users Java API. AppEngineDataStoreFactoryספריית הלקוח של Google HTTP עבור Java היא אפשרות טובה לשמירת פרטי הכניסה באמצעות ממשק ה-API של Google App Engine Data Store.

קוד לדוגמה:

public class AppEngineSample extends AbstractAppEngineAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

public class AppEngineCallbackSample extends AbstractAppEngineAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

זרימת קוד ההרשאה של שורת הפקודה

קוד לדוגמה פשוט שנלקח מ-dailymotion-cmdline-sample:

/** Authorizes the installed application to access user's protected data. */
private static Credential authorize() throws Exception {
  OAuth2ClientCredentials.errorIfNotSpecified();
  // set up authorization code flow
  AuthorizationCodeFlow flow = new AuthorizationCodeFlow.Builder(BearerToken
      .authorizationHeaderAccessMethod(),
      HTTP_TRANSPORT,
      JSON_FACTORY,
      new GenericUrl(TOKEN_SERVER_URL),
      new ClientParametersAuthentication(
          OAuth2ClientCredentials.API_KEY, OAuth2ClientCredentials.API_SECRET),
      OAuth2ClientCredentials.API_KEY,
      AUTHORIZATION_SERVER_URL).setScopes(Arrays.asList(SCOPE))
      .setDataStoreFactory(DATA_STORE_FACTORY).build();
  // authorize
  LocalServerReceiver receiver = new LocalServerReceiver.Builder().setHost(
      OAuth2ClientCredentials.DOMAIN).setPort(OAuth2ClientCredentials.PORT).build();
  return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
}

private static void run(HttpRequestFactory requestFactory) throws IOException {
  DailyMotionUrl url = new DailyMotionUrl("https://api.dailymotion.com/videos/favorites");
  url.setFields("id,tags,title,url");

  HttpRequest request = requestFactory.buildGetRequest(url);
  VideoFeed videoFeed = request.execute().parseAs(VideoFeed.class);
  ...
}

public static void main(String[] args) {
  ...
  DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR);
  final Credential credential = authorize();
  HttpRequestFactory requestFactory =
      HTTP_TRANSPORT.createRequestFactory(new HttpRequestInitializer() {
        @Override
        public void initialize(HttpRequest request) throws IOException {
          credential.initialize(request);
          request.setParser(new JsonObjectParser(JSON_FACTORY));
        }
      });
  run(requestFactory);
  ...
}

תהליך לקוח מבוסס דפדפן

אלו השלבים הטיפוסיים של זרימת הלקוח מבוססת-הדפדפן, שמפורטים במפרט Implicit Grant:

  • באמצעות BrowserClientRequestUrl, הפניה אוטומטית של הדפדפן של משתמש הקצה לדף ההרשאה שבו משתמש הקצה יכול להעניק לאפליקציה גישה לנתונים המוגנים שלו.
  • משתמשים באפליקציית JavaScript כדי לעבד את אסימון הגישה שנמצא בקטע של כתובת ה-URL ב-URI של ההפניה האוטומטית שרשום בשרת ההרשאות.

דוגמה לשימוש באפליקציית אינטרנט:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
  String url = new BrowserClientRequestUrl(
      "https://server.example.com/authorize", "s6BhdRkqt3").setState("xyz")
      .setRedirectUri("https://client.example.com/cb").build();
  response.sendRedirect(url);
}

זיהוי של אסימון גישה שפג תוקפו

על פי מפרט OAuth 2.0, כשהשרת מופעל כדי לגשת למשאב מוגן עם אסימון גישה שפג תוקפו, השרת מגיב בדרך כלל עם קוד הסטטוס 401 Unauthorized של HTTP, כמו בדוגמה הבאה:

   HTTP/1.1 401 Unauthorized
   WWW-Authenticate: Bearer realm="example",
                     error="invalid_token",
                     error_description="The access token expired"

עם זאת, נראה שיש מידה רבה של גמישות במפרט. לקבלת פרטים, יש לעיין בתיעוד של ספק OAuth 2.0.

גישה חלופית היא לבדוק את הפרמטר expires_in בתגובה לאסימון הגישה. הערך הזה מציין את משך החיים בשניות של אסימון הגישה שהוענק, שהוא בדרך כלל שעה. עם זאת, יכול להיות שהתוקף של אסימון הגישה לא יפוג בפועל בסוף התקופה הזו, ויכול להיות שהשרת ימשיך לאפשר גישה. לכן, בדרך כלל מומלץ להמתין לקוד הסטטוס 401 Unauthorized ולא להניח שתוקף האסימון פג על סמך הזמן שחלף. לחלופין, אתם יכולים לנסות לרענן את אסימון הגישה זמן קצר לפני שתוקפו יפוג, ואם שרת האסימונים לא זמין, תוכלו להמשיך להשתמש באסימון הגישה עד שתקבלו 401. זוהי השיטה שבה משתמשים כברירת מחדל ב-Credential.

אפשרות אחרת היא לקבל אסימון גישה חדש לפני כל בקשה. אבל כדי להשתמש בו נדרשת בקשת HTTP נוספת לשרת האסימונים, כך שככל הנראה הבחירה לא טובה מבחינת מהירות ושימוש ברשת. באופן אידאלי, כדאי לאחסן את אסימון הגישה באחסון קבוע ומאובטח כדי לצמצם את הבקשות של אפליקציות לאסימוני גישה חדשים. (אבל עבור יישומים מותקנים, אחסון מאובטח הוא בעיה קשה).

שימו לב שאסימון גישה עלול להפוך ללא חוקי מסיבות אחרות, שלא קשורות להתוקף שלו. לדוגמה, אם המשתמש ביטל את האסימון באופן מפורש, לכן חשוב שהקוד לטיפול בשגיאות יהיה חזק. אחרי שאתם מזהים אסימון שכבר לא תקף, למשל אם פג התוקף שלו או אם הוא בוטל, אתם צריכים להסיר את אסימון הגישה מהאחסון. למשל, ב-Android צריך לבצע קריאה ל-AccountManager.invalidateAuthToken.