저장된 사용자 인증 정보로 사용자 로그인 처리

원탭 로그인 클라이언트를 사용하여 사용자가 이전에 앱에 로그인할 때 사용한 사용자 인증 정보 중 하나를 검색할 권한을 요청합니다. 이러한 사용자 인증 정보는 Google 계정이거나 Chrome, Android 자동 완성 또는 비밀번호 대용 Smart Lock을 사용하여 Google에 저장한 사용자 이름과 비밀번호 조합일 수 있습니다.

원탭 로그인 UI

사용자 인증 정보가 성공적으로 검색되면 이를 사용하여 사용자가 앱에 원활하게 로그인하도록 할 수 있습니다.

사용자가 사용자 인증 정보를 저장하지 않았다면 UI가 표시되지 않으며 개발자는 일반적인 로그아웃 환경을 제공할 수 있습니다.

원탭 로그인은 어디에서 사용해야 하나요?

앱에서 사용자에게 로그인을 요청하는 경우 로그인 화면에 원탭 UI를 표시합니다. 이는 이미 'Google 계정으로 로그인' 버튼이 있는 경우에도 유용할 수 있습니다. 원탭 UI는 사용자가 이전에 로그인에 사용한 사용자 인증 정보만 표시하도록 구성할 수 있으므로, 이전 로그인 방법을 자주 사용하지 않는 사용자에게 알림이 될 수 있으며, 사용자가 실수로 앱에서 새 계정을 만드는 것을 방지할 수 있습니다.

앱에서 로그인이 선택사항인 경우 로그인하여 향상된 환경이 제공되는 모든 화면에서 원탭 로그인을 사용하는 것이 좋습니다. 예를 들어 사용자가 로그아웃한 상태에서 앱으로 콘텐츠를 탐색할 수 있지만 로그인 후에는 댓글을 게시하거나 장바구니에 상품을 추가할 수만 있다면 원탭 로그인에 적합한 컨텍스트가 됩니다.

로그인 옵션 앱은 위에 설명된 이유로 로그인 화면에서 원탭 로그인을 사용해야 합니다.

시작하기 전에

1. 원탭 로그인 클라이언트 구성

저장된 비밀번호, 저장된 Google 계정 또는 둘 중 하나로 사용자를 로그인시키도록 원탭 로그인 클라이언트를 구성할 수 있습니다. 신규 사용자의 경우 원탭 계정 생성을 사용 설정하고 최대한 많은 재사용자의 자동 또는 원탭 로그인을 사용 설정하기 위해 두 가지를 모두 지원하는 것이 좋습니다.

앱에서 비밀번호 기반 로그인을 사용하는 경우 setPasswordRequestOptions()를 사용하여 비밀번호 사용자 인증 정보 요청을 사용 설정합니다.

앱에서 Google 로그인을 사용하는 경우 setGoogleIdTokenRequestOptions()를 사용하여 Google ID 토큰 요청을 사용 설정하고 구성합니다.

  • 서버 클라이언트 ID를 Google API 콘솔에서 만든 ID로 설정합니다. 이는 Android 클라이언트 ID가 아닌 서버의 클라이언트 ID입니다.

  • 승인된 계정을 기준으로 필터링하도록 클라이언트를 구성합니다. 이 옵션을 사용 설정하면 원탭 클라이언트는 이전에 이미 사용한 Google 계정으로 앱에 로그인하라는 메시지만 표시합니다. 이렇게 하면 사용자가 이미 계정을 가지고 있는지, 어떤 Google 계정을 사용했는지 잘 모르는 경우에 로그인할 수 있으며, 사용자가 실수로 앱에서 새 계정을 만드는 것을 방지할 수 있습니다.

  • 가능한 경우 자동으로 사용자가 로그인하도록 하려면 setAutoSelectEnabled()로 이 기능을 사용 설정합니다. 다음 기준이 충족되면 자동 로그인이 가능합니다.

    • 앱에 저장된 사용자의 사용자 인증 정보가 정확히 한 개입니다. 즉, 저장된 비밀번호 또는 저장된 Google 계정이 한 개입니다.
    • 사용자가 Google 계정 설정에서 자동 로그인을 사용 중지하지 않았습니다.
  • 선택사항이지만, 로그인 보안을 강화하고 재생 공격을 방지하려면 nonce를 사용하는 것이 좋습니다. setNonce를 사용하여 각 요청에 nonce를 포함하세요. nonce 생성에 관한 제안과 추가 세부정보는 SafetyNet의 nonce 가져오기 섹션을 참고하세요.

Java

public class YourActivity extends AppCompatActivity {
  // ...

  private SignInClient oneTapClient;
  private BeginSignInRequest signInRequest;

  @Override
  public void onCreate(@Nullable Bundle savedInstanceState,
                       @Nullable PersistableBundle persistentState) {
      super.onCreate(savedInstanceState, persistentState);

      oneTapClient = Identity.getSignInClient(this);
      signInRequest = BeginSignInRequest.builder()
              .setPasswordRequestOptions(PasswordRequestOptions.builder()
                      .setSupported(true)
                      .build())
              .setGoogleIdTokenRequestOptions(GoogleIdTokenRequestOptions.builder()
                      .setSupported(true)
                      // Your server's client ID, not your Android client ID.
                      .setServerClientId(getString(R.string.default_web_client_id))
                      // Only show accounts previously used to sign in.
                      .setFilterByAuthorizedAccounts(true)
                      .build())
              // Automatically sign in when exactly one credential is retrieved.
              .setAutoSelectEnabled(true)
              .build();
      // ...
  }
  // ...
}

Kotlin

class YourActivity : AppCompatActivity() {
    // ...

    private lateinit var oneTapClient: SignInClient
    private lateinit var signInRequest: BeginSignInRequest

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        oneTapClient = Identity.getSignInClient(this)
        signInRequest = BeginSignInRequest.builder()
            .setPasswordRequestOptions(BeginSignInRequest.PasswordRequestOptions.builder()
                .setSupported(true)
                .build())
            .setGoogleIdTokenRequestOptions(
                BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                    .setSupported(true)
                    // Your server's client ID, not your Android client ID.
                    .setServerClientId(getString(R.string.your_web_client_id))
                    // Only show accounts previously used to sign in.
                    .setFilterByAuthorizedAccounts(true)
                    .build())
            // Automatically sign in when exactly one credential is retrieved.
            .setAutoSelectEnabled(true)
            .build()
        // ...
    }
    // ...
}

2. 로그인한 사용자 확인

로그인한 사용자나 로그아웃한 사용자가 활동을 사용할 수 있는 경우 원탭 로그인 UI를 표시하기 전에 사용자의 상태를 확인하세요.

또한 메시지를 닫거나 프롬프트 외부를 탭하여 사용자가 원탭 로그인을 이미 거부했는지 추적해야 합니다. 이는 Activity의 부울 속성만큼 간단할 수 있습니다. 아래의 원탭 UI 표시 중지하기를 참고하세요.

3. 원탭 로그인 UI 표시

사용자가 로그인하지 않았고 원탭 로그인 사용을 아직 거부하지 않았다면 클라이언트 객체의 beginSignIn() 메서드를 호출하고 반환되는 Task에 리스너를 연결합니다. 앱은 일반적으로 Activity의 onCreate() 메서드에서 이 작업을 실행하거나 단일 활동 아키텍처를 사용할 때는 화면 전환 후에 실행합니다.

원탭 클라이언트는 사용자에게 저장된 앱 사용자 인증 정보가 있는 경우 성공 리스너를 호출합니다. 성공 리스너의 Task 결과에서 대기 중인 인텐트를 가져와 startIntentSenderForResult()에 전달하여 원탭 로그인 UI를 시작합니다.

사용자에게 저장된 사용자 인증 정보가 없으면 원탭 클라이언트는 실패 리스너를 호출합니다. 이 경우에는 별도의 조치가 필요하지 않습니다. 앱의 로그아웃 환경을 계속 표시하기만 하면 됩니다. 그러나 원탭 가입을 지원하는 경우 여기에서 해당 절차를 시작하여 원활한 계정 생성 환경을 만들 수 있습니다. 탭 한 번으로 새 계정 만들기를 참조하세요.

Java

oneTapClient.beginSignIn(signUpRequest)
        .addOnSuccessListener(this, new OnSuccessListener<BeginSignInResult>() {
            @Override
            public void onSuccess(BeginSignInResult result) {
                try {
                    startIntentSenderForResult(
                            result.getPendingIntent().getIntentSender(), REQ_ONE_TAP,
                            null, 0, 0, 0);
                } catch (IntentSender.SendIntentException e) {
                    Log.e(TAG, "Couldn't start One Tap UI: " + e.getLocalizedMessage());
                }
            }
        })
        .addOnFailureListener(this, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // No saved credentials found. Launch the One Tap sign-up flow, or
                // do nothing and continue presenting the signed-out UI.
                Log.d(TAG, e.getLocalizedMessage());
            }
        });

Kotlin

oneTapClient.beginSignIn(signInRequest)
    .addOnSuccessListener(this) { result ->
        try {
            startIntentSenderForResult(
                result.pendingIntent.intentSender, REQ_ONE_TAP,
                null, 0, 0, 0, null)
        } catch (e: IntentSender.SendIntentException) {
            Log.e(TAG, "Couldn't start One Tap UI: ${e.localizedMessage}")
        }
    }
    .addOnFailureListener(this) { e ->
        // No saved credentials found. Launch the One Tap sign-up flow, or
        // do nothing and continue presenting the signed-out UI.
        Log.d(TAG, e.localizedMessage)
    }

4. 사용자 응답 처리

원탭 로그인 메시지에 대한 사용자의 응답은 활동의 onActivityResult() 메서드를 사용하여 앱에 보고됩니다. 사용자가 로그인을 선택하면 저장된 사용자 인증 정보가 결과로 반환됩니다. 사용자가 원탭 UI를 닫거나 외부를 탭하여 로그인을 거부하면 결과에 RESULT_CANCELED 코드가 반환됩니다. 앱은 두 가지 가능성을 모두 처리해야 합니다.

가져온 사용자 인증 정보로 로그인

사용자가 사용자 인증 정보를 앱과 공유하기로 한 경우 개발자는 onActivityResult()의 인텐트 데이터를 원탭 클라이언트의 getSignInCredentialFromIntent() 메서드로 전달하여 사용자 인증 정보를 가져올 수 있습니다. 사용자가 앱과 Google 계정 사용자 인증 정보를 공유한 경우에는 사용자 인증 정보에 null이 아닌 googleIdToken 속성이 있고, 사용자가 저장된 비밀번호를 공유한 경우에는 null이 아닌 password 속성이 있습니다.

사용자 인증 정보를 사용하여 앱의 백엔드에 인증합니다.

  • 사용자 이름 및 비밀번호 쌍을 가져온 경우 사용자가 수동으로 제공한 것과 동일한 방식으로 로그인합니다.
  • Google 계정 사용자 인증 정보를 검색했으면 ID 토큰을 사용하여 백엔드로 인증합니다. 재생 공격을 방지하기 위해 nonce를 사용하기로 선택한 경우 백엔드 서버의 응답 값을 확인합니다. ID 토큰을 사용하여 백엔드 인증을 참조하세요.

Java

public class YourActivity extends AppCompatActivity {

  // ...
  private static final int REQ_ONE_TAP = 2;  // Can be any integer unique to the Activity.
  private boolean showOneTapUI = true;
  // ...

  @Override
  protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
      super.onActivityResult(requestCode, resultCode, data);

      switch (requestCode) {
          case REQ_ONE_TAP:
              try {
                  SignInCredential credential = oneTapClient.getSignInCredentialFromIntent(data);
                  String idToken = credential.getGoogleIdToken();
                  String username = credential.getId();
                  String password = credential.getPassword();
                  if (idToken !=  null) {
                      // Got an ID token from Google. Use it to authenticate
                      // with your backend.
                      Log.d(TAG, "Got ID token.");
                  } else if (password != null) {
                      // Got a saved username and password. Use them to authenticate
                      // with your backend.
                      Log.d(TAG, "Got password.");
                  }
              } catch (ApiException e) {
                  // ...
              }
              break;
      }
  }
}

Kotlin

class YourActivity : AppCompatActivity() {

    // ...
    private val REQ_ONE_TAP = 2  // Can be any integer unique to the Activity
    private var showOneTapUI = true
    // ...

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) {
             REQ_ONE_TAP -> {
                try {
                    val credential = oneTapClient.getSignInCredentialFromIntent(data)
                    val idToken = credential.googleIdToken
                    val username = credential.id
                    val password = credential.password
                    when {
                        idToken != null -> {
                            // Got an ID token from Google. Use it to authenticate
                            // with your backend.
                            Log.d(TAG, "Got ID token.")
                        }
                        password != null -> {
                            // Got a saved username and password. Use them to authenticate
                            // with your backend.
                            Log.d(TAG, "Got password.")
                        }
                        else -> {
                            // Shouldn't happen.
                            Log.d(TAG, "No ID token or password!")
                        }
                    }
                } catch (e: ApiException) {
                    // ...
                }
            }
        }
    }
    // ...
}

원탭 UI 표시 중지

사용자가 로그인을 거부한 경우 getSignInCredentialFromIntent()를 호출하면 CommonStatusCodes.CANCELED 상태 코드와 함께 ApiException이 발생합니다. 이 경우 반복 메시지로 사용자를 귀찮게 하지 않도록 원탭 로그인 UI를 일시적으로 사용 중지해야 합니다. 다음 예에서는 활동에 원탭 로그인을 제공할지 결정하는 데 사용하는 속성을 활동에 설정하여 이를 실행합니다. 하지만 SharedPreferences에 값을 저장하거나 다른 메서드를 사용할 수도 있습니다.

원탭 로그인 메시지의 비율 제한을 자체적으로 구현하는 것이 중요합니다. 취소하지 않고 사용자가 여러 프롬프트를 연속으로 취소하면 원탭 클라이언트에서 다음 24시간 동안 사용자에게 메시지를 표시하지 않습니다.

Java

public class YourActivity extends AppCompatActivity {

  // ...
  private static final int REQ_ONE_TAP = 2;  // Can be any integer unique to the Activity.
  private boolean showOneTapUI = true;
  // ...

  @Override
  protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
      super.onActivityResult(requestCode, resultCode, data);

      switch (requestCode) {
          case REQ_ONE_TAP:
              try {
                  // ...
              } catch (ApiException e) {
                  switch (e.getStatusCode()) {
                      case CommonStatusCodes.CANCELED:
                          Log.d(TAG, "One-tap dialog was closed.");
                          // Don't re-prompt the user.
                          showOneTapUI = false;
                          break;
                      case CommonStatusCodes.NETWORK_ERROR:
                          Log.d(TAG, "One-tap encountered a network error.");
                          // Try again or just ignore.
                          break;
                      default:
                          Log.d(TAG, "Couldn't get credential from result."
                                  + e.getLocalizedMessage());
                          break;
                  }
              }
              break;
      }
  }
}

Kotlin

class YourActivity : AppCompatActivity() {

    // ...
    private val REQ_ONE_TAP = 2  // Can be any integer unique to the Activity
    private var showOneTapUI = true
    // ...

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) {
            REQ_ONE_TAP -> {
                try {
                    // ...
                } catch (e: ApiException) {
                    when (e.statusCode) {
                        CommonStatusCodes.CANCELED -> {
                            Log.d(TAG, "One-tap dialog was closed.")
                            // Don't re-prompt the user.
                            showOneTapUI = false
                        }
                        CommonStatusCodes.NETWORK_ERROR -> {
                            Log.d(TAG, "One-tap encountered a network error.")
                            // Try again or just ignore.
                        }
                        else -> {
                            Log.d(TAG, "Couldn't get credential from result." +
                                " (${e.localizedMessage})")
                        }
                    }
                }
            }
        }
    }
    // ...
}

5. 로그아웃 처리

사용자가 앱에서 로그아웃하면 원탭 클라이언트의 signOut() 메서드를 호출합니다. signOut()를 호출하면 사용자가 다시 로그인할 때까지 자동 로그인이 사용 중지됩니다.

자동 로그인을 사용하지 않더라도 이 단계는 사용자가 앱에서 로그아웃할 때 개발자가 사용하는 Play 서비스 API의 인증 상태도 재설정되도록 하기 때문에 중요합니다.

다음 단계

Google 사용자 인증 정보를 검색하도록 원탭 클라이언트를 구성한 경우 이제 앱에서 사용자의 Google 계정을 나타내는 Google ID 토큰을 가져올 수 있습니다. 백엔드에서 이러한 토큰을 사용하는 방법을 알아보세요.

Google 로그인을 지원하는 경우 원탭 클라이언트를 사용하여 앱에 원활한 계정 생성 흐름을 추가할 수도 있습니다.