서버 측 앱용 Google 로그인

사용자가 오프라인 상태일 때 사용자 대신 Google 서비스를 사용하려면 사용자가 JavaScript API 클라이언트를 사용하여 클라이언트 측에서 앱을 승인하고 서버에 특별한 일회성 승인 코드를 전송하는 하이브리드 서버 측 흐름을 사용해야 합니다. 서버는 이 일회용 코드를 교환하여 Google에서 자체 액세스 및 갱신 토큰을 얻어 서버가 자체 API 호출을 할 수 있도록 하며, 이러한 작업은 사용자가 오프라인일 때도 가능합니다. 이 일회성 코드 흐름은 순수한 서버 측 흐름 및 서버에 액세스 토큰을 전송하는 것에 비해 보안상의 이점이 있습니다.

서버 측 애플리케이션의 액세스 토큰을 가져오는 로그인 과정은 다음과 같습니다.

일회성 코드에는 여러 가지 보안상의 이점이 있습니다. Google은 코드를 사용하여 중개자 없이 서버에 직접 토큰을 제공합니다. 코드 유출은 권장되지 않지만 클라이언트 보안 비밀번호 없이는 사용하기가 매우 어렵습니다. 클라이언트 보안 비밀을 지켜주세요.

일회성 코드 흐름 구현

Google 로그인 버튼은 액세스 토큰승인 코드를 모두 제공합니다. 이 코드는 서버가 액세스 토큰을 받기 위해 Google 서버와 교환할 수 있는 일회성 코드입니다.

다음 샘플 코드는 일회성 코드 흐름을 실행하는 방법을 보여줍니다.

일회성 코드 흐름으로 Google 로그인을 인증하려면 다음을 수행해야 합니다.

1단계: 클라이언트 ID 및 클라이언트 보안 비밀번호 만들기

클라이언트 ID와 클라이언트 보안 비밀번호를 만들려면 Google API 콘솔 프로젝트를 만들고 OAuth 클라이언트 ID를 설정하고 자바스크립트 출처를 등록합니다.

  1. Google API 콘솔로 이동합니다.

  2. 프로젝트 드롭다운에서 기존 프로젝트를 선택하거나 새 프로젝트 만들기를 선택하여 새 프로젝트를 만듭니다.

  3. 'API 및 서비스' 아래의 사이드바에서 사용자 인증 정보를 선택한 다음 동의 화면 구성을 클릭합니다.

    이메일 주소를 선택하고 제품 이름을 지정한 다음 저장을 누릅니다.

  4. 사용자 인증 정보 탭에서 사용자 인증 정보 만들기 드롭다운 목록을 선택하고 OAuth 클라이언트 ID를 선택합니다.

  5. 애플리케이션 유형 아래에서 웹 애플리케이션을 선택합니다.

    다음과 같이 앱이 Google API에 액세스할 수 있는 출처를 등록합니다. 출처는 프로토콜, 호스트 이름, 포트의 고유한 조합입니다.

    1. 승인된 JavaScript 출처 필드에 앱의 출처를 입력합니다. 여러 출처를 입력하여 앱이 다양한 프로토콜, 도메인 또는 하위 도메인에서 실행되도록 허용할 수 있습니다. 와일드카드는 사용할 수 없습니다. 아래 예에서 두 번째 URL이 프로덕션 URL일 수 있습니다.

      http://localhost:8080
      https://myproductionurl.example.com
      
    2. 승인된 리디렉션 URI 필드에는 값이 필요하지 않습니다. 리디렉션 URI는 JavaScript API와 함께 사용되지 않습니다.

    3. 만들기 버튼을 누릅니다.

  6. 표시되는 OAuth 클라이언트 대화상자에서 클라이언트 ID를 복사합니다. 클라이언트 ID를 사용하면 앱에서 사용 설정된 Google API에 액세스할 수 있습니다.

2단계: 페이지에 Google 플랫폼 라이브러리 포함

index.html 웹페이지의 DOM에 스크립트를 삽입하는 익명 함수를 보여주는 다음 스크립트를 포함합니다.

<!-- The top of file index.html -->
<html itemscope itemtype="http://schema.org/Article">
<head>
  <!-- BEGIN Pre-requisites -->
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js">
  </script>
  <script src="https://apis.google.com/js/client:platform.js?onload=start" async defer>
  </script>
  <!-- END Pre-requisites -->

3단계: GoogleAuth 객체 초기화하기

auth2 라이브러리를 로드하고 gapi.auth2.init()를 호출하여 GoogleAuth 객체를 초기화합니다. init()를 호출할 때 클라이언트 ID와 요청하려는 범위를 지정합니다.

<!-- Continuing the <head> section -->
  <script>
    function start() {
      gapi.load('auth2', function() {
        auth2 = gapi.auth2.init({
          client_id: 'YOUR_CLIENT_ID.apps.googleusercontent.com',
          // Scopes to request in addition to 'profile' and 'email'
          //scope: 'additional_scope'
        });
      });
    }
  </script>
</head>
<body>
  <!-- ... -->
</body>
</html>

4단계: 페이지에 로그인 버튼 추가

웹페이지에 로그인 버튼을 추가하고 클릭 핸들러를 연결하여 grantOfflineAccess()를 호출하여 일회성 코드 흐름을 시작합니다.

<!-- Add where you want your sign-in button to render -->
<!-- Use an image that follows the branding guidelines in a real app -->
<button id="signinButton">Sign in with Google</button>
<script>
  $('#signinButton').click(function() {
    // signInCallback defined in step 6.
    auth2.grantOfflineAccess().then(signInCallback);
  });
</script>

5단계: 사용자 로그인

사용자가 로그인 버튼을 클릭하고 요청한 권한에 액세스할 수 있는 권한을 앱에 부여합니다. 그러면 grantOfflineAccess().then() 메서드에서 지정한 콜백 함수에 승인 코드와 함께 JSON 객체가 전달됩니다. 예를 들면 다음과 같습니다.

{"code":"4/yU4cQZTMnnMtetyFcIWNItG32eKxxxgXXX-Z4yyJJJo.4qHskT-UtugceFc0ZRONyF4z7U4UmAI"}

6단계: 서버로 승인 코드 전송

code는 서버가 자체 액세스 토큰 및 갱신 토큰으로 교환할 수 있는 일회용 코드입니다. 오프라인 액세스를 요청하는 승인 대화상자가 사용자에게 표시된 후에만 갱신 토큰을 얻을 수 있습니다. 4단계의 OfflineAccessOptions에서 select-account prompt를 지정한 경우 이후 교환이 갱신 토큰에 대해 null를 반환하므로 나중에 사용할 수 있도록 검색하는 갱신 토큰을 저장해야 합니다. 이 흐름은 표준 OAuth 2.0 흐름보다 강화된 보안을 제공합니다.

액세스 토큰은 항상 유효한 승인 코드 교환 시 반환됩니다.

다음 스크립트는 로그인 버튼에 대한 콜백 함수를 정의합니다. 로그인이 성공하면 함수가 클라이언트 측에서 사용할 액세스 토큰을 저장하고 동일한 도메인의 서버에 일회성 코드를 전송합니다.

<!-- Last part of BODY element in file index.html -->
<script>
function signInCallback(authResult) {
  if (authResult['code']) {

    // Hide the sign-in button now that the user is authorized, for example:
    $('#signinButton').attr('style', 'display: none');

    // Send the code to the server
    $.ajax({
      type: 'POST',
      url: 'http://example.com/storeauthcode',
      // Always include an `X-Requested-With` header in every AJAX request,
      // to protect against CSRF attacks.
      headers: {
        'X-Requested-With': 'XMLHttpRequest'
      },
      contentType: 'application/octet-stream; charset=utf-8',
      success: function(result) {
        // Handle or verify the server response.
      },
      processData: false,
      data: authResult['code']
    });
  } else {
    // There was an error.
  }
}
</script>

7단계: 액세스 토큰용 승인 코드 교환

서버에서 인증 코드를 액세스 토큰과 갱신 토큰으로 교환합니다. 액세스 토큰을 사용하여 사용자 대신 Google API를 호출하고, 선택적으로 갱신 토큰을 저장하여 액세스 토큰이 만료될 때 새 액세스 토큰을 얻을 수 있습니다.

프로필 액세스를 요청한 경우 사용자의 기본 프로필 정보가 포함된 ID 토큰도 가져옵니다.

예를 들면 다음과 같습니다.

Java
// (Receive authCode via HTTPS POST)


if (request.getHeader("X-Requested-With") == null) {
  // Without the `X-Requested-With` header, this request could be forged. Aborts.
}

// Set path to the Web application client_secret_*.json file you downloaded from the
// Google API Console: https://console.cloud.google.com/apis/credentials
// You can also find your Web application client ID and client secret from the
// console and specify them directly when you create the GoogleAuthorizationCodeTokenRequest
// object.
String CLIENT_SECRET_FILE = "/path/to/client_secret.json";

// Exchange auth code for access token
GoogleClientSecrets clientSecrets =
    GoogleClientSecrets.load(
        JacksonFactory.getDefaultInstance(), new FileReader(CLIENT_SECRET_FILE));
GoogleTokenResponse tokenResponse =
          new GoogleAuthorizationCodeTokenRequest(
              new NetHttpTransport(),
              JacksonFactory.getDefaultInstance(),
              "https://oauth2.googleapis.com/token",
              clientSecrets.getDetails().getClientId(),
              clientSecrets.getDetails().getClientSecret(),
              authCode,
              REDIRECT_URI)  // Specify the same redirect URI that you use with your web
                             // app. If you don't have a web version of your app, you can
                             // specify an empty string.
              .execute();

String accessToken = tokenResponse.getAccessToken();

// Use access token to call API
GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
Drive drive =
    new Drive.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential)
        .setApplicationName("Auth Code Exchange Demo")
        .build();
File file = drive.files().get("appfolder").execute();

// Get profile info from ID token
GoogleIdToken idToken = tokenResponse.parseIdToken();
GoogleIdToken.Payload payload = idToken.getPayload();
String userId = payload.getSubject();  // Use this value as a key to identify a user.
String email = payload.getEmail();
boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
String locale = (String) payload.get("locale");
String familyName = (String) payload.get("family_name");
String givenName = (String) payload.get("given_name");
Python
from apiclient import discovery
import httplib2
from oauth2client import client

# (Receive auth_code by HTTPS POST)


# If this request does not have `X-Requested-With` header, this could be a CSRF
if not request.headers.get('X-Requested-With'):
    abort(403)

# Set path to the Web application client_secret_*.json file you downloaded from the
# Google API Console: https://console.cloud.google.com/apis/credentials
CLIENT_SECRET_FILE = '/path/to/client_secret.json'

# Exchange auth code for access token, refresh token, and ID token
credentials = client.credentials_from_clientsecrets_and_code(
    CLIENT_SECRET_FILE,
    ['https://www.googleapis.com/auth/drive.appdata', 'profile', 'email'],
    auth_code)

# Call Google API
http_auth = credentials.authorize(httplib2.Http())
drive_service = discovery.build('drive', 'v3', http=http_auth)
appfolder = drive_service.files().get(fileId='appfolder').execute()

# Get profile info from ID token
userid = credentials.id_token['sub']
email = credentials.id_token['email']