인증은 사용자가 누구인지 확인하며 일반적으로 사용자 가입 또는 로그인이라고 합니다. 승인은 데이터 또는 리소스에 대한 액세스 권한을 부여하거나 거부하는 프로세스입니다. 예를 들어 앱이 사용자의 Google Drive에 액세스하기 위해 사용자의 동의를 요청합니다.
인증 및 승인 호출은 앱의 필요에 따라 별도의 두 가지 흐름이어야 합니다.
앱에 Google API 데이터를 사용할 수 있지만 앱의 핵심 기능의 일부로 필요하지 않은 기능이 있는 경우 API 데이터에 액세스할 수 없는 경우를 원활하게 처리할 수 있도록 앱을 설계해야 합니다. 예를 들어 사용자가 Drive 액세스 권한을 부여하지 않은 경우 최근에 저장된 파일 목록을 숨길 수 있습니다.
사용자가 특정 API에 대한 액세스가 필요한 작업을 실행할 때만 Google API에 액세스하는 데 필요한 범위에 대한 액세스를 요청해야 합니다. 예를 들어 사용자가 'Drive에 저장' 버튼을 탭할 때마다 사용자의 Drive에 액세스할 수 있는 권한을 요청해야 합니다.
인증과 승인을 분리하면 신규 사용자가 압도되거나 특정 권한을 요청받는 이유에 대해 혼동하는 것을 방지할 수 있습니다.
인증에는 Credential Manager API를 사용하는 것이 좋습니다. Google에 저장된 사용자 데이터에 액세스해야 하는 작업을 승인하려면 AuthorizationClient를 사용하는 것이 좋습니다.
프로젝트 설정
- 에서 프로젝트를 열거나 아직 프로젝트가 없는 경우 프로젝트를 만듭니다.
- 에서 모든 정보가 완전하고 정확한지 확인합니다.
- 앱에 올바른 앱 이름, 앱 로고, 앱 홈페이지가 할당되어 있는지 확인합니다. 이러한 값은 가입 시 Google 계정으로 로그인 동의 화면과 서드 파티 앱 및 서비스 화면에 사용자에게 표시됩니다.
- 앱의 개인정보처리방침 및 서비스 약관 URL을 지정했는지 확인합니다.
- 에서 아직 앱의 Android 클라이언트 ID가 없다면 만듭니다. 앱의 패키지 이름과 SHA-1 서명을 지정해야 합니다.
- 에서 아직 만들지 않은 경우 새 '웹 애플리케이션' 클라이언트 ID를 만듭니다. 지금은 '승인된 JavaScript 원본' 및 '승인된 리디렉션 URI' 필드를 무시해도 됩니다. 이 클라이언트 ID는 백엔드 서버가 Google의 인증 서비스와 통신할 때 백엔드 서버를 식별하는 데 사용됩니다.
종속 항목 선언
모듈의 build.gradle 파일에서 Google ID 서비스 라이브러리의 최신 버전을 사용하여 종속 항목을 선언합니다.
dependencies {
// ... other dependencies
implementation "com.google.android.gms:play-services-auth:21.4.0"
}
사용자 작업에 필요한 권한 요청
사용자가 추가 범위가 필요한 작업을 실행할 때마다 AuthorizationClient.authorize()
를 호출합니다. 예를 들어 사용자가 Drive 앱 저장소에 대한 액세스가 필요한 작업을 실행하는 경우 다음을 실행합니다.
Kotlin
val requestedScopes: List<Scope> = listOf(DriveScopes.DRIVE_FILE)
val authorizationRequest = AuthorizationRequest.builder()
.setRequestedScopes(requestedScopes)
.build()
Identity.getAuthorizationClient(activity)
.authorize(authorizationRequestBuilder.build())
.addOnSuccessListener { authorizationResult ->
if (authorizationResult.hasResolution()) {
val pendingIntent = authorizationResult.pendingIntent
// Access needs to be granted by the user
startAuthorizationIntent.launchIntentSenderRequest.Builder(pendingIntent!!.intentSender).build()
} else {
// Access was previously granted, continue with user action
saveToDriveAppFolder(authorizationResult);
}
}
.addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) }
자바
List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_FILE);
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
.setRequestedScopes(requestedScopes)
.build();
Identity.getAuthorizationClient(activity)
.authorize(authorizationRequest)
.addOnSuccessListener(authorizationResult -> {
if (authorizationResult.hasResolution()) {
// Access needs to be granted by the user
startAuthorizationIntent.launch(
new IntentSenderRequest.Builder(
authorizationResult.getPendingIntent().getIntentSender()
).build()
);
} else {
// Access was previously granted, continue with user action
saveToDriveAppFolder(authorizationResult);
}
})
.addOnFailureListener(e -> Log.e(TAG, "Failed to authorize", e));
ActivityResultLauncher
를 정의할 때는 다음 스니펫과 같이 응답을 처리합니다. 여기서는 프래그먼트에서 처리한다고 가정합니다. 코드는 필요한 권한이 부여되었는지 확인한 후 사용자 작업을 실행합니다.
Kotlin
private lateinit var startAuthorizationIntent: ActivityResultLauncher<IntentSenderRequest>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
// ...
startAuthorizationIntent =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { activityResult ->
try {
// extract the result
val authorizationResult = Identity.getAuthorizationClient(requireContext())
.getAuthorizationResultFromIntent(activityResult.data)
// continue with user action
saveToDriveAppFolder(authorizationResult);
} catch (ApiException e) {
// log exception
}
}
}
자바
private ActivityResultLauncher<IntentSenderRequest> startAuthorizationIntent;
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// ...
startAuthorizationIntent =
registerForActivityResult(
new ActivityResultContracts.StartIntentSenderForResult(),
activityResult -> {
try {
// extract the result
AuthorizationResult authorizationResult =
Identity.getAuthorizationClient(requireActivity())
.getAuthorizationResultFromIntent(activityResult.getData());
// continue with user action
saveToDriveAppFolder(authorizationResult);
} catch (ApiException e) {
// log exception
}
});
}
서버 측에서 Google API에 액세스하는 경우 AuthorizationResult
에서 getServerAuthCode()
메서드를 호출하여 액세스 및 갱신 토큰으로 교환하기 위해 백엔드로 전송하는 승인 코드를 가져옵니다. 자세한 내용은 사용자 데이터에 대한 지속적인 액세스 권한 유지를 참고하세요.
사용자 데이터 또는 리소스에 대한 권한 취소
이전에 부여된 액세스 권한을 취소하려면 AuthorizationClient.revokeAccess()
을 호출합니다. 예를 들어 사용자가 앱에서 계정을 삭제하고 앱에 이전에 DriveScopes.DRIVE_FILE
액세스 권한이 부여된 경우 다음 코드를 사용하여 액세스 권한을 취소합니다.
Kotlin
val requestedScopes: MutableList<Scope> = mutableListOf(DriveScopes.DRIVE_FILE)
RevokeAccessRequest revokeAccessRequest = RevokeAccessRequest.builder()
.setAccount(account)
.setScopes(requestedScopes)
.build()
Identity.getAuthorizationClient(activity)
.revokeAccess(revokeAccessRequest)
.addOnSuccessListener { Log.i(TAG, "Successfully revoked access") }
.addOnFailureListener { e -> Log.e(TAG, "Failed to revoke access", e) }
자바
List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_FILE);
RevokeAccessRequest revokeAccessRequest = RevokeAccessRequest.builder()
.setAccount(account)
.setScopes(requestedScopes)
.build();
Identity.getAuthorizationClient(activity)
.revokeAccess(revokeAccessRequest)
.addOnSuccessListener(unused -> Log.i(TAG, "Successfully revoked access"))
.addOnFailureListener(e -> Log.e(TAG, "Failed to revoke access", e));
토큰 캐시 지우기
OAuth 액세스 토큰은 서버에서 수신 시 로컬로 캐시되어 액세스 속도를 높이고 네트워크 호출을 줄입니다. 이러한 토큰은 만료되면 캐시에서 자동으로 삭제되지만 다른 이유로 무효화될 수도 있습니다.
토큰을 사용할 때 IllegalStateException
가 표시되면 로컬 캐시를 지워 액세스 토큰에 대한 다음 승인 요청이 OAuth 서버로 전송되도록 합니다. 다음 스니펫은 로컬 캐시에서 invalidAccessToken
을 삭제합니다.
Kotlin
Identity.getAuthorizationClient(activity)
.clearToken(ClearTokenRequest.builder().setToken(invalidAccessToken).build())
.addOnSuccessListener { Log.i(TAG, "Successfully removed the token from the cache") }
.addOnFailureListener{ e -> Log.e(TAG, "Failed to clear token", e) }
자바
Identity.getAuthorizationClient(activity)
.clearToken(ClearTokenRequest.builder().setToken(invalidAccessToken).build())
.addOnSuccessListener(unused -> Log.i(TAG, "Successfully removed the token from the cache"))
.addOnFailureListener(e -> Log.e(TAG, "Failed to clear the token cache", e));
승인 중에 사용자 정보 가져오기
승인 응답에는 사용된 사용자 계정에 관한 정보가 포함되지 않습니다. 응답에는 요청된 범위의 토큰만 포함됩니다. 예를 들어 사용자의 Google Drive에 액세스하기 위한 액세스 토큰을 가져오는 응답은 사용자의 드라이브에 있는 파일에 액세스하는 데 사용할 수 있지만 사용자가 선택한 계정의 ID를 표시하지 않습니다. 사용자 이름이나 이메일과 같은 정보를 가져오려면 다음 옵션을 사용하세요.
승인을 요청하기 전에 인증 관리자 API를 사용하여 사용자를 Google 계정으로 로그인시킵니다. 인증 관리자의 인증 응답에는 이메일 주소와 같은 사용자 정보가 포함되며 앱의 기본 계정을 선택한 계정으로 설정합니다. 필요한 경우 앱에서 이 계정을 추적할 수 있습니다. 후속 승인 요청은 계정을 기본값으로 사용하고 승인 흐름에서 계정 선택 단계를 건너뜁니다. 승인에 다른 계정을 사용하려면 기본이 아닌 계정에서 승인을 참고하세요.
승인 요청에서 원하는 범위 (예:
Drive scope
) 외에userinfo
,profile
,openid
범위를 요청합니다. 액세스 토큰이 반환되면 원하는 HTTP 라이브러리를 사용하여 OAuth userinfo 엔드포인트(https://www.googleapis.com/oauth2/v3/userinfo)에GET
HTTP 요청을 하고 헤더에 수신한 액세스 토큰을 포함하여 사용자 정보를 가져옵니다. 이는 다음curl
명령과 동일합니다.curl -X GET \ "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" \ -H "Authorization: Bearer $TOKEN"
응답은 요청된 범위로 제한되고 JSON 형식으로 지정된
UserInfo
입니다.
기본 계정이 아닌 계정에서 승인
사용자 인증 정보를 관리자를 사용하여 인증하고 AuthorizationClient.authorize()
를 실행하면 앱의 기본 계정이 사용자가 선택한 계정으로 설정됩니다. 즉, 이후의 모든 승인 호출은 이 기본 계정을 사용합니다. 계정 선택기를 강제로 표시하려면 인증 관리자의 clearCredentialState()
API를 사용하여 앱에서 사용자를 로그아웃합니다.
사용자 데이터에 대한 지속적인 액세스 유지
앱에서 사용자 데이터에 액세스해야 하는 경우 AuthorizationClient.authorize()
를 한 번 호출합니다. 후속 세션에서 사용자가 부여된 권한을 삭제하지 않는 한 동일한 메서드를 호출하여 사용자 상호작용 없이 목표를 달성하기 위한 액세스 토큰을 획득합니다. 반면 백엔드 서버에서 오프라인 모드로 사용자의 데이터에 액세스해야 하는 경우에는 '새로고침 토큰'이라는 다른 유형의 토큰을 요청해야 합니다.
액세스 토큰은 의도적으로 수명이 짧게 설계되어 있으며 수명은 1시간입니다. 액세스 토큰이 가로채거나 도용된 경우 제한된 유효 기간으로 인해 잠재적인 오용이 최소화됩니다. 만료되면 토큰이 무효화되고 이를 사용하려는 시도는 리소스 서버에서 거부됩니다. 액세스 토큰은 수명이 짧으므로 서버는 갱신 토큰을 사용하여 사용자의 데이터에 대한 지속적인 액세스를 유지합니다. 갱신 토큰은 클라이언트가 이전 액세스 토큰이 만료된 경우 사용자 상호작용 없이 승인 서버에서 단기 액세스 토큰을 요청하는 데 사용하는 수명이 긴 토큰입니다.
갱신 토큰을 얻으려면 먼저 앱의 승인 단계에서 '오프라인 액세스'를 요청하여 인증 코드(또는 승인 코드)를 얻은 다음 서버에서 인증 코드를 갱신 토큰으로 교환해야 합니다. 장기 갱신 토큰은 새 액세스 토큰을 얻는 데 반복적으로 사용될 수 있으므로 서버에 안전하게 저장하는 것이 중요합니다. 따라서 보안 문제로 인해 기기에 갱신 토큰을 저장하는 것은 권장되지 않습니다. 대신 액세스 토큰 교환이 이루어지는 앱의 백엔드 서버에 저장해야 합니다.
인증 코드가 앱의 백엔드 서버로 전송되면 계정 승인 가이드의 단계에 따라 서버에서 단기 액세스 토큰과 장기 갱신 토큰으로 교환할 수 있습니다. 이 교환은 앱의 백엔드에서만 발생해야 합니다.
Kotlin
// Ask for offline access during the first authorization request
val authorizationRequest = AuthorizationRequest.builder()
.setRequestedScopes(requestedScopes)
.requestOfflineAccess(serverClientId)
.build()
Identity.getAuthorizationClient(activity)
.authorize(authorizationRequest)
.addOnSuccessListener { authorizationResult ->
startAuthorizationIntent.launchIntentSenderRequest.Builder(
pendingIntent!!.intentSender
).build()
}
.addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) }
자바
// Ask for offline access during the first authorization request
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
.setRequestedScopes(requestedScopes)
.requestOfflineAccess(serverClientId)
.build();
Identity.getAuthorizationClient(getContext())
.authorize(authorizationRequest)
.addOnSuccessListener(authorizationResult -> {
startAuthorizationIntent.launch(
new IntentSenderRequest.Builder(
authorizationResult.getPendingIntent().getIntentSender()
).build()
);
})
.addOnFailureListener(e -> Log.e(TAG, "Failed to authorize"));
다음 스니펫에서는 프래그먼트에서 승인이 시작된다고 가정합니다.
Kotlin
private lateinit var startAuthorizationIntent: ActivityResultLauncher<IntentSenderRequest>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
// ...
startAuthorizationIntent =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { activityResult ->
try {
val authorizationResult = Identity.getAuthorizationClient(requireContext())
.getAuthorizationResultFromIntent(activityResult.data)
// short-lived access token
accessToken = authorizationResult.accessToken
// store the authorization code used for getting a refresh token safely to your app's backend server
val authCode: String = authorizationResult.serverAuthCode
storeAuthCodeSafely(authCode)
} catch (e: ApiException) {
// log exception
}
}
}
Java
private ActivityResultLauncher<IntentSenderRequest> startAuthorizationIntent;
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// ...
startAuthorizationIntent =
registerForActivityResult(
new ActivityResultContracts.StartIntentSenderForResult(),
activityResult -> {
try {
AuthorizationResult authorizationResult =
Identity.getAuthorizationClient(requireActivity())
.getAuthorizationResultFromIntent(activityResult.getData());
// short-lived access token
accessToken = authorizationResult.getAccessToken();
// store the authorization code used for getting a refresh token safely to your app's backend server
String authCode = authorizationResult.getServerAuthCode()
storeAuthCodeSafely(authCode);
} catch (ApiException e) {
// log exception
}
});
}