OAuth2 서비스 인증

인증 토큰 로직 다이어그램
그림 1. Android 계정 관리자에서 유효한 인증 토큰을 가져오는 절차

온라인 서비스에 안전하게 액세스하려면 사용자는 신원을 증명하여 서비스에 인증해야 합니다. 서드 파티 서비스에 액세스하는 애플리케이션의 경우 보안 문제가 훨씬 더 복잡합니다. 사용자가 서비스에 액세스하려면 인증을 받아야 할 뿐만 아니라, 사용자를 대신하여 작업할 수 있도록 애플리케이션이 승인되어야 합니다.

서드 파티 서비스 인증을 처리하는 업계 표준 방법은 OAuth2 프로토콜입니다. OAuth2는 사용자 ID와 사용자를 대신할 수 있는 애플리케이션의 승인을 모두 나타내는 인증 토큰이라는 단일 값을 제공합니다. 이 강의에서는 OAuth2를 지원하는 Google 서버에 연결하는 방법을 설명합니다. Google 서비스를 예로 사용했지만, 여기서 설명하는 기법은 OAuth2 프로토콜을 올바르게 지원하는 모든 서비스에서 작동합니다.

OAuth2를 사용하면 다음에 유용합니다.

  • 사용자로부터 계정을 사용하여 온라인 서비스에 액세스할 권한을 얻습니다.
  • 사용자를 대신해 온라인 서비스에 인증합니다.
  • 인증 오류를 처리합니다.

정보 수집하기

OAuth2를 사용하려면 액세스하려는 서비스에 대한 몇 가지 API 관련 사항을 알아야 합니다.

  • 액세스하려는 서비스의 URL입니다.
  • 인증 범위: 앱이 요청하는 특정 액세스 유형을 정의하는 문자열입니다. 예를 들어 Google Tasks 읽기 전용 액세스의 인증 범위는 View your tasks이고 Google Tasks 읽기-쓰기 액세스의 인증 범위는 Manage your tasks입니다.
  • 클라이언트 ID와 클라이언트 보안 비밀번호: 서비스에서 앱을 식별하는 문자열입니다. 이러한 문자열은 서비스 소유자로부터 직접 가져와야 합니다. Google에는 클라이언트 ID와 보안 비밀을 가져오기 위한 셀프 서비스 시스템이 있습니다.

인터넷 권한 요청하기

Android 6.0 (API 수준 23) 이상을 타겟팅하는 앱의 경우 getAuthToken() 메서드 자체에는 권한이 필요하지 않습니다. 그러나 토큰 작업을 실행하려면 다음 코드 스니펫과 같이 매니페스트 파일에 INTERNET 권한을 추가해야 합니다.

<manifest ... >
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

인증 토큰 요청하기

토큰을 가져오려면 AccountManager.getAuthToken()을 호출합니다.

주의: 일부 계정 작업에는 네트워크 통신이 포함될 수 있으므로 대부분의 AccountManager 메서드는 비동기식입니다. 즉, 모든 인증 작업을 하나의 함수로 실행하는 대신 일련의 콜백으로 구현해야 합니다.

다음 스니펫은 일련의 콜백을 사용하여 토큰을 가져오는 방법을 보여줍니다.

Kotlin

val am: AccountManager = AccountManager.get(this)
val options = Bundle()

am.getAuthToken(
        myAccount_,                     // Account retrieved using getAccountsByType()
        "Manage your tasks",            // Auth scope
        options,                        // Authenticator-specific options
        this,                           // Your activity
        OnTokenAcquired(),              // Callback called when a token is successfully acquired
        Handler(OnError())              // Callback called if an error occurs
)

Java

AccountManager am = AccountManager.get(this);
Bundle options = new Bundle();

am.getAuthToken(
    myAccount_,                     // Account retrieved using getAccountsByType()
    "Manage your tasks",            // Auth scope
    options,                        // Authenticator-specific options
    this,                           // Your activity
    new OnTokenAcquired(),          // Callback called when a token is successfully acquired
    new Handler(new OnError()));    // Callback called if an error occurs

이 예에서 OnTokenAcquiredAccountManagerCallback를 구현하는 클래스입니다. AccountManagerBundle가 포함된 AccountManagerFuture를 사용하여 OnTokenAcquired에서 run()를 호출합니다. 호출이 성공하면 토큰은 Bundle 내에 있습니다.

다음은 Bundle에서 토큰을 가져오는 방법입니다.

Kotlin

private class OnTokenAcquired : AccountManagerCallback<Bundle> {

    override fun run(result: AccountManagerFuture<Bundle>) {
        // Get the result of the operation from the AccountManagerFuture.
        val bundle: Bundle = result.getResult()

        // The token is a named value in the bundle. The name of the value
        // is stored in the constant AccountManager.KEY_AUTHTOKEN.
        val token: String = bundle.getString(AccountManager.KEY_AUTHTOKEN)
    }
}

Java

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
    @Override
    public void run(AccountManagerFuture<Bundle> result) {
        // Get the result of the operation from the AccountManagerFuture.
        Bundle bundle = result.getResult();

        // The token is a named value in the bundle. The name of the value
        // is stored in the constant AccountManager.KEY_AUTHTOKEN.
        String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
        ...
    }
}

모두 순조롭게 진행되면 BundleKEY_AUTHTOKEN 키에 유효한 토큰이 포함되어 있으므로 설정이 완료된 것입니다.

첫 번째 인증 토큰 요청이 다음과 같은 이유로 실패할 수 있습니다.

  • 기기나 네트워크의 오류로 인해 AccountManager가 실패했습니다.
  • 사용자가 앱에 계정 액세스 권한을 부여하지 않기로 했습니다.
  • 저장된 계정 사용자 인증 정보가 충분하지 않아 계정 액세스 권한을 얻지 못했습니다.
  • 캐시된 인증 토큰이 만료되었습니다.

애플리케이션은 처음 두 사례를 사소하게 처리할 수 있습니다. 일반적으로 사용자에게 오류 메시지를 표시하기만 하면 됩니다. 네트워크가 다운되거나 사용자가 액세스 권한을 부여하지 않기로 결정했다면 애플리케이션에서 할 수 있는 일이 많지 않습니다. 마지막 두 사례는 좀 더 복잡합니다. 제대로 작동하는 애플리케이션은 이러한 실패를 자동으로 처리해야 하기 때문입니다.

세 번째 실패 사례인 사용자 인증 정보가 충분하지 않으면 AccountManagerCallback에서 수신한 Bundle(이전 예시의 OnTokenAcquired)를 통해 전달됩니다. BundleKEY_INTENT 키에 Intent가 포함되어 있으면 인증자는 사용자와 직접 상호작용해야 유효한 토큰을 제공한다고 개발자에게 알려줍니다.

인증자에서 Intent가 반환되는 데는 많은 이유가 있을 수 있습니다. 사용자가 이 계정에 처음 로그인했을 수 있습니다. 사용자의 계정이 만료되어 다시 로그인해야 하거나 저장된 사용자 인증 정보가 잘못되었을 수 있습니다. 계정에 2단계 인증이 필요하거나 망막 스캔을 위해 카메라를 활성화해야 할 수도 있습니다. 이유는 중요하지 않습니다. 유효한 토큰을 원한다면 Intent를 실행하여 토큰을 얻어야 합니다.

Kotlin

private inner class OnTokenAcquired : AccountManagerCallback<Bundle> {

    override fun run(result: AccountManagerFuture<Bundle>) {
        val launch: Intent? = result.getResult().get(AccountManager.KEY_INTENT) as? Intent
        if (launch != null) {
            startActivityForResult(launch, 0)
        }
    }
}

Java

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
    @Override
    public void run(AccountManagerFuture<Bundle> result) {
        ...
        Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT);
        if (launch != null) {
            startActivityForResult(launch, 0);
            return;
        }
    }
}

이 예에서는 startActivityForResult()를 사용하므로 자체 활동에 onActivityResult()를 구현하여 Intent의 결과를 캡처할 수 있습니다. 이 점이 중요합니다. 인증자의 응답 Intent에서 결과를 캡처하지 않으면 사용자가 성공적으로 인증되었는지 알 수 없습니다.

결과가 RESULT_OK이면 인증자가 저장된 사용자 인증 정보를 업데이트하여 개발자가 요청한 액세스 수준에 충분하므로 AccountManager.getAuthToken()를 다시 호출하여 새 인증 토큰을 요청해야 합니다.

토큰이 만료된 마지막 사례는 실제로 AccountManager 실패가 아닙니다. 토큰이 만료되었는지 여부를 확인하는 유일한 방법은 서버에 연결하는 것입니다. AccountManager가 지속적으로 온라인에서 모든 토큰의 상태를 확인하는 것은 낭비이며 비용이 많이 듭니다. 따라서 이는 개발자의 애플리케이션이 인증 토큰을 사용하여 온라인 서비스에 액세스하려고 할 때만 감지될 수 있는 실패입니다.

온라인 서비스에 연결하기

아래의 예는 Google 서버에 연결하는 방법을 보여줍니다. Google은 업계 표준 OAuth2 프로토콜을 사용하여 요청을 인증하므로 여기서 설명하는 기법이 광범위하게 적용될 수 있습니다. 그러나 모든 서버가 다르다는 것을 명심하십시오. 특정 상황을 고려하여 이 안내를 약간 조정해야 할 수 있습니다.

Google API를 사용할 때는 각 요청에 API 키, 클라이언트 ID, 클라이언트 비밀번호, 인증 키 등 4가지 값을 제공해야 합니다. 처음 세 개는 Google API 콘솔 웹사이트에서 가져온 것입니다 마지막 값은 AccountManager.getAuthToken()를 호출하여 얻은 문자열 값입니다. 이를 HTTP 요청의 일부로 Google 서버에 전달합니다.

Kotlin

val url = URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=$your_api_key")
val conn = url.openConnection() as HttpURLConnection
conn.apply {
    addRequestProperty("client_id", your client id)
    addRequestProperty("client_secret", your client secret)
    setRequestProperty("Authorization", "OAuth $token")
}

Java

URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + your_api_key);
URLConnection conn = (HttpURLConnection) url.openConnection();
conn.addRequestProperty("client_id", your client id);
conn.addRequestProperty("client_secret", your client secret);
conn.setRequestProperty("Authorization", "OAuth " + token);

요청이 HTTP 오류 코드 401을 반환하면 토큰이 거부된 것입니다. 마지막 섹션에서 설명한 것처럼 토큰이 만료되는 가장 일반적인 이유는 다음과 같습니다. 해결 방법은 간단합니다. AccountManager.invalidateAuthToken()를 호출하고 토큰 획득 프로세스를 한 번 더 반복하면 됩니다.

토큰이 만료되는 경우가 매우 흔하고 문제 해결도 쉽기 때문에 많은 애플리케이션은 토큰을 요청하기도 전에 토큰이 만료되었다고 가정합니다. 토큰 갱신이 서버에서 비용이 적게 드는 작업인 경우 AccountManager.getAuthToken()를 처음 호출하기 전에 AccountManager.invalidateAuthToken()를 호출하여 인증 토큰을 두 번 요청할 필요가 없도록 할 수 있습니다.