개요
목적: 이 문서에서는 Java용 Google OAuth 클라이언트 라이브러리에서 제공하는 일반적인 OAuth 2.0 함수를 설명합니다. 이러한 함수는 모든 인터넷 서비스의 인증 및 승인에 사용할 수 있습니다.
GoogleCredential
를 사용하여 Google 서비스에서 OAuth 2.0 승인을 수행하는 방법에 관한 안내는 Java용 Google API 클라이언트 라이브러리에서 OAuth 2.0 사용을 참고하세요.
요약: OAuth 2.0은 최종 사용자가 클라이언트 애플리케이션에서 보호되는 서버 측 리소스에 액세스하도록 안전하게 승인할 수 있는 표준 사양입니다. 또한 OAuth 2.0 보유자 토큰 사양에서는 최종 사용자 승인 프로세스 중에 부여된 액세스 토큰을 사용하여 이러한 보호된 리소스에 액세스하는 방법을 설명합니다.
자세한 내용은 다음 패키지의 Javadoc 문서를 참고하세요.
- com.google.api.client.auth.oauth2(google-oauth-client)
- com.google.api.client.extensions.servlet.auth.oauth2 (google-oauth-client-servlet)
- com.google.api.client.extensions.appengine.auth.oauth2 (from google-oauth-client-appengine)
클라이언트 등록
Java용 Google OAuth 클라이언트 라이브러리를 사용하기 전에 승인 서버에 애플리케이션을 등록하여 클라이언트 ID와 클라이언트 보안 비밀을 수신해야 할 수 있습니다. 이 프로세스에 관한 일반적인 내용은 클라이언트 등록 사양을 참고하세요.
사용자 인증 정보 및 사용자 인증 정보 저장소
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 구현은 지원 중단되었으며 향후 출시에서 삭제될 예정입니다. 또는 Java용 Google HTTP 클라이언트 라이브러리에서 제공하는 DataStoreFactory 및 DataStore 인터페이스를 StoredCredential과 함께 사용하는 방법이 있습니다.
라이브러리에서 제공하는 다음 구현 중 하나를 사용할 수 있습니다.
- JdoDataStoreFactory는 JDO를 사용하여 사용자 인증 정보를 유지합니다.
- AppEngineDataStoreFactory는 Google App Engine Data Store API를 사용하여 사용자 인증 정보를 유지합니다.
- MemoryDataStoreFactory는 메모리에 사용자 인증 정보를 '유지'합니다. 이는 프로세스 전체 기간 동안의 단기 저장소로만 유용합니다.
- FileDataStoreFactory는 사용자 인증 정보를 파일에 유지합니다.
Google App Engine 사용자:
AppEngineCredentialStore가 지원 중단되고 삭제됩니다.
StoredCredential과 함께 AppEngineDataStoreFactory를 사용하는 것이 좋습니다. 이전 방식으로 저장된 사용자 인증 정보가 있는 경우 추가된 도우미 메서드 migrateTo(AppEngineDataStoreFactory) 또는 migrateTo(DataStore)를 사용하여 이전할 수 있습니다.
DataStoreCredentialRefreshListener를 사용하고 GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener)를 사용하여 사용자 인증 정보에 설정합니다.
승인 코드 흐름
인증 코드 흐름을 사용하여 최종 사용자가 애플리케이션에 보호된 데이터에 대한 액세스 권한을 부여하도록 허용합니다. 이 흐름의 프로토콜은 승인 코드 부여 사양에 지정되어 있습니다.
이 흐름은 AuthorizationCodeFlow를 사용하여 구현됩니다. 단계는 다음과 같습니다.
- 최종 사용자가 애플리케이션에 로그인합니다. 이 사용자를 애플리케이션에 고유한 사용자 ID와 연결해야 합니다.
- 사용자 ID를 기반으로 AuthorizationCodeFlow.loadCredential(String)을 호출하여 사용자의 사용자 인증 정보가 이미 알려져 있는지 확인합니다. 이 경우 더 이상 필요한 작업이 없습니다.
- 그렇지 않은 경우 AuthorizationCodeFlow.newAuthorizationUrl()을 호출하고 최종 사용자의 브라우저를 승인 페이지로 안내합니다. 이 페이지에서 애플리케이션에 보호된 데이터에 대한 액세스 권한을 부여할 수 있습니다.
- 그러면 웹브라우저에서 '코드' 쿼리 매개변수가 포함된 리디렉션 URL로 리디렉션합니다. 이 매개변수는 AuthorizationCodeFlow.newTokenRequest(String)를 사용하여 액세스 토큰을 요청하는 데 사용할 수 있습니다.
- AuthorizationCodeFlow.createAndStoreCredential(TokenResponse, String)을 사용하여 보호된 리소스에 액세스하기 위한 사용자 인증 정보를 저장하고 가져옵니다.
또는 AuthorizationCodeFlow를 사용하지 않는 경우 하위 클래스를 사용할 수 있습니다.
- DataStore.get(String)을 사용하여 사용자 ID를 기반으로 저장소에서 사용자 인증 정보를 로드합니다.
- AuthorizationCodeRequestUrl을 사용하여 브라우저를 승인 페이지로 안내합니다.
- AuthorizationCodeResponseUrl을 사용하여 승인 응답을 처리하고 승인 코드를 파싱합니다.
- AuthorizationCodeTokenRequest를 사용하여 액세스 토큰과 갱신 토큰을 요청합니다.
- 새 사용자 인증 정보를 만들고 DataStore.set(String, V)를 사용하여 저장합니다.
- 사용자 인증 정보를 사용하여 보호된 리소스에 액세스합니다. 만료된 액세스 토큰은 해당하는 경우 갱신 토큰을 사용하여 자동으로 갱신됩니다. DataStoreCredentialRefreshListener를 사용하고 Credential.Builder.addRefreshListener(CredentialRefreshListener)를 사용하여 사용자 인증 정보에 설정해야 합니다.
Servlet 승인 코드 흐름
이 라이브러리는 servlet 도우미 클래스를 제공하여 기본 사용 사례의 승인 코드 흐름을 크게 단순화합니다. (google-oauth-client-servlet에서) AbstractAuthorizationCodeCallbackServlet 및 AbstractAuthorizationCodeCallbackServlet의 구체적인 서브클래스를 제공하고 web.xml 파일에 추가하기만 하면 됩니다.AbstractAuthorizationCodeServlet 웹 애플리케이션의 사용자 로그인을 처리하고 사용자 ID를 추출해야 합니다.
샘플 코드
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의 승인 코드 흐름은 Google App Engine의 Users Java API를 활용할 수 있다는 점을 제외하고 servlet 승인 코드 흐름과 거의 동일합니다. Users Java API를 사용 설정하려면 사용자가 로그인해야 합니다. 아직 로그인하지 않은 사용자를 로그인 페이지로 리디렉션하는 방법에 대한 자세한 내용은 보안 및 인증(web.xml)을 참조하세요.
servlet 케이스와의 주요 차이점은 google-oauth-client-appengine의 AbstractAppEngineAuthorizationCodeServlet 및 AbstractAppEngineAuthorizationCodeCallbackServlet의 구체적인 서브클래스를 제공한다는 점입니다. 추상 서블릿 클래스를 확장하고 Users Java API를 사용하여 getUserId
메서드를 자동으로 구현합니다. Java용 Google HTTP 클라이언트 라이브러리의 AppEngineDataStoreFactory는 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 애플리케이션을 사용하여 승인 서버에 등록된 리디렉션 URI의 URL 프래그먼트에 있는 액세스 토큰을 처리합니다.
웹 애플리케이션의 샘플 사용법:
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 bearer 사양에 따라 서버가 만료된 액세스 토큰으로 보호된 리소스에 액세스하도록 호출되면 서버는 일반적으로 다음과 같은 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
를 수신할 때까지 액세스 토큰을 계속 사용하세요. 이는 Credential에서 기본적으로 사용되는 전략입니다.
다른 방법은 모든 요청 전에 새 액세스 토큰을 가져오는 것이지만, 이 경우 매번 토큰 서버에 HTTP 요청을 추가로 해야 하므로 속도와 네트워크 사용량 측면에서 좋지 않은 선택일 수 있습니다. 애플리케이션의 새 액세스 토큰 요청을 최소화하려면 액세스 토큰을 안전한 영구 저장소에 저장하는 것이 좋습니다. 하지만 설치된 애플리케이션의 경우 보안 저장소는 어려운 문제입니다.
액세스 토큰은 만료 이외의 이유(예: 사용자가 토큰을 명시적으로 취소한 경우)로 인해 유효하지 않게 될 수 있으므로 오류 처리 코드가 강력한지 확인하세요. 토큰이 더 이상 유효하지 않다고 감지되면(예: 만료되었거나 취소된 경우) 저장소에서 액세스 토큰을 삭제해야 합니다. 예를 들어 Android에서는 AccountManager.invalidateAuthToken을 호출해야 합니다.