OAuth 2.0 และไลบรารีของไคลเอ็นต์ Google OAuth สำหรับ Java

ภาพรวม

จุดประสงค์: เอกสารนี้อธิบายฟังก์ชันทั่วไปของ OAuth 2.0 ที่ไลบรารีของไคลเอ็นต์ Google OAuth สำหรับ Java คุณสามารถใช้ฟังก์ชันเหล่านี้เพื่อ ตรวจสอบสิทธิ์และให้สิทธิ์สำหรับบริการอินเทอร์เน็ต

ดูวิธีใช้ GoogleCredential เพื่อให้สิทธิ์ OAuth 2.0 กับบริการของ Google ที่หัวข้อการใช้ OAuth 2.0 กับไลบรารีของไคลเอ็นต์ Google API สำหรับ Java

สรุป: OAuth 2.0 คือข้อกำหนดมาตรฐานสำหรับการอนุญาตให้ผู้ใช้ปลายทางให้สิทธิ์แอปพลิเคชันไคลเอ็นต์ในการเข้าถึงทรัพยากรฝั่งเซิร์ฟเวอร์ที่มีการป้องกันได้อย่างปลอดภัย นอกจากนี้ ข้อกําหนดของโทเค็นสำหรับผู้ถือ OAuth 2.0 จะอธิบายวิธีเข้าถึงทรัพยากรที่มีการป้องกันโดยใช้โทเค็นเพื่อการเข้าถึงที่ได้รับอนุญาตในระหว่างกระบวนการให้สิทธิ์ผู้ใช้ปลายทาง

โปรดดูรายละเอียดในเอกสารประกอบ Javadoc สำหรับแพ็กเกจต่อไปนี้

การลงทะเบียนลูกค้า

ก่อนใช้ไลบรารีของไคลเอ็นต์ Google OAuth สำหรับ Java คุณอาจต้องลงทะเบียนแอปพลิเคชันกับเซิร์ฟเวอร์การให้สิทธิ์เพื่อรับรหัสไคลเอ็นต์และรหัสลับไคลเอ็นต์ (ดูข้อมูลทั่วไปเกี่ยวกับกระบวนการนี้ได้ที่ข้อกำหนดการจดทะเบียนไคลเอ็นต์)

ข้อมูลเข้าสู่ระบบและที่เก็บข้อมูลเข้าสู่ระบบ

ข้อมูลเข้าสู่ระบบเป็นคลาสตัวช่วย 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 ซึ่งมาจากไลบรารีของไคลเอ็นต์ HTTP ของ Google สำหรับ Java

คุณสามารถใช้วิธีการอย่างใดอย่างหนึ่งต่อไปนี้ที่ไลบรารีมีให้

  • JdoDataStoreFactory ใช้ข้อมูลรับรองโดยใช้ JDO
  • AppEngineDataStoreFactory ใช้ข้อมูลรับรองโดยใช้ Google App Engine Data Store API
  • 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

ไลบรารีนี้มีคลาสตัวช่วยของเซิร์ฟเล็ตเพื่อลดความซับซ้อนของขั้นตอนรหัสการให้สิทธิ์สำหรับกรณีการใช้งานขั้นพื้นฐานได้อย่างมาก คุณเพียงแค่ระบุคลาสย่อยที่เป็นรูปธรรมของ 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 จะเหมือนกับขั้นตอนรหัสการให้สิทธิ์ของเซิร์ฟเล็ต เว้นแต่ว่าเราจะใช้ประโยชน์จาก Users Java API ของ Google App Engine ได้ ผู้ใช้ต้องเข้าสู่ระบบเพื่อเปิดใช้ User Java API จึงจะเปิดใช้ได้ สำหรับข้อมูลเกี่ยวกับการเปลี่ยนเส้นทางผู้ใช้ไปยังหน้าเข้าสู่ระบบในกรณีที่ยังไม่ได้เข้าสู่ระบบ โปรดดูความปลอดภัยและการตรวจสอบสิทธิ์ (ใน web.xml)

ความแตกต่างหลักจากกรณีของเซิร์ฟเล็ตคือคุณระบุคลาสย่อยของ AbstractAppEngineAuthorizationCodeServlet และ AbstractAppEngineAuthorizationCodeCallbackServlet (จาก google-oauth-client-appengine) ส่วนขยายประเภทนี้ขยายคลาสของเซิร์ฟเล็ตนามธรรมและใช้เมธอด getUserId ให้คุณโดยใช้ Users Java API AppEngineDataStoreFactory (จากไลบรารีของไคลเอ็นต์ HTTP ของ Google สำหรับ Java เป็นตัวเลือกที่ดีสำหรับการรักษาข้อมูลเข้าสู่ระบบโดยใช้ Google App Engine Data Store API

โค้ดตัวอย่าง

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);
  ...
}

ขั้นตอนของไคลเอ็นต์ที่อิงตามเบราว์เซอร์

ขั้นตอนต่อไปนี้เป็นขั้นตอนปกติของโฟลว์ไคลเอ็นต์บนเบราว์เซอร์ที่ระบุไว้ในข้อกำหนดการให้สิทธิ์แบบไม่เจาะจงปลายทาง

  • ใช้ 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 เมื่อมีการเรียกเซิร์ฟเวอร์ให้เข้าถึงทรัพยากรที่มีการป้องกันด้วยโทเค็นเพื่อการเข้าถึงที่หมดอายุแล้ว เซิร์ฟเวอร์มักจะตอบสนองด้วยรหัสสถานะ HTTP 401 Unauthorized ดังตัวอย่างต่อไปนี้

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

อย่างไรก็ตาม ดูเหมือนว่าข้อกำหนดจะมีความยืดหยุ่นมาก โปรดดูรายละเอียดในเอกสารประกอบของผู้ให้บริการ OAuth 2.0

อีกวิธีหนึ่งคือการตรวจสอบพารามิเตอร์ expires_in ในการตอบกลับโทเค็นเพื่อการเข้าถึง ค่านี้จะระบุอายุการใช้งานเป็นวินาทีของโทเค็นเพื่อการเข้าถึงที่ได้รับสิทธิ์ ซึ่งปกติจะอยู่ที่ 1 ชั่วโมง อย่างไรก็ตาม โทเค็นเพื่อการเข้าถึงอาจไม่หมดอายุเมื่อสิ้นสุดระยะเวลานั้น และเซิร์ฟเวอร์อาจยังอนุญาตให้เข้าถึงได้ต่อไป ด้วยเหตุนี้ โดยทั่วไปเราจึงแนะนำให้รอรหัสสถานะ 401 Unauthorized แทนที่จะถือว่าโทเค็นหมดอายุตามเวลาที่ผ่านไป หรือคุณจะลองรีเฟรชโทเค็นเพื่อการเข้าถึงโดยเร็วก่อนหมดอายุก็ได้ และหากเซิร์ฟเวอร์โทเค็นไม่พร้อมใช้งาน ให้ใช้โทเค็นเพื่อการเข้าถึงต่อไปจนกว่าจะได้รับ 401 ซึ่งเป็นกลยุทธ์ที่ใช้โดยค่าเริ่มต้นในข้อมูลเข้าสู่ระบบ

อีกตัวเลือกหนึ่งคือการเก็บโทเค็นเพื่อการเข้าถึงใหม่ก่อนคำขอทุกครั้ง แต่ต้องใช้คำขอ HTTP เพิ่มเติมไปยังเซิร์ฟเวอร์โทเค็นทุกครั้ง ดังนั้นจึงเป็นตัวเลือกที่ไม่ดีในแง่ของความเร็วและการใช้งานเครือข่าย ตามหลักแล้ว ให้เก็บโทเค็นเพื่อการเข้าถึงไว้ในพื้นที่เก็บข้อมูลถาวรที่ปลอดภัยเพื่อลดคำขอของโทเค็นเพื่อการเข้าถึงใหม่ของแอปพลิเคชันให้เหลือน้อยที่สุด (แต่สำหรับแอปพลิเคชันที่ติดตั้งไว้นั้น การจัดเก็บที่ปลอดภัยถือเป็นปัญหายาก)

โปรดทราบว่าโทเค็นเพื่อการเข้าถึงอาจไม่ถูกต้องด้วยเหตุผลอื่นนอกเหนือจากการหมดอายุ เช่น หากผู้ใช้เพิกถอนโทเค็นอย่างชัดเจน ดังนั้นให้ตรวจสอบว่าโค้ดการจัดการข้อผิดพลาดมีประสิทธิภาพ เมื่อคุณตรวจพบว่าโทเค็นไม่สามารถใช้ได้อีกต่อไป เช่น หากโทเค็นหมดอายุหรือถูกเพิกถอน คุณต้องนำโทเค็นเพื่อการเข้าถึงออกจากพื้นที่เก็บข้อมูล ตัวอย่างเช่น ใน Android คุณต้องเรียก AccountManager.invalidateAuthToken