為 Android 應用程式新增多重驗證機制
本文說明如何在 Android 應用程式中新增簡訊多重驗證機制。
多重驗證可以提升應用程式的安全性。攻擊者通常會竊取密碼及盜用社群媒體帳戶,但攔截簡訊的難度較高。
事前準備
- 至少啟用一個支援多重驗證的供應商。除了電話驗證、匿名驗證和 Apple Game Center 以外,所有供應商都支援 MFA。 
- 請確認您的應用程式會驗證使用者的電子郵件地址。多重驗證需要通過電子郵件驗證。這麼做可防止惡意人士使用不屬於自己的電子郵件地址註冊服務,然後透過新增第二個因素將真正的擁有者鎖在門外。 
- 在 Firebase 主控台註冊應用程式的 SHA-1 雜湊 (變更內容會自動轉移至 Google Cloud Identity Platform)。 - 請按照「驗證用戶端」一文中的步驟,取得應用程式的 SHA-1 雜湊值。 
- 開啟 Firebase 控制台。 
- 前往「專案設定」。 
- 在「你的應用程式」下方,按一下 Android 圖示。 
- 按照指示步驟新增 SHA-1 雜湊。 
 
啟用多重驗證
- 前往 Google Cloud 控制台的「Identity Platform MFA」頁面。 
 前往多重驗證
- 在「多重驗證」中,按一下「啟用」。 
- 輸入您要用來測試應用程式的電話號碼。雖然這不是必要步驟,我們仍強烈建議您註冊測試電話號碼,以免在開發期間發生節流情形。 
- 如果您尚未授權應用程式的網域,請按一下「新增網域」,將網域加入許可清單。 
- 按一下 [儲存]。 
選擇註冊模式
您可以選擇應用程式是否需要多重驗證,以及註冊使用者的方式和時間。常見的模式包括:
- 在註冊過程中註冊使用者的第二重驗證。如果您的應用程式要求所有使用者都必須進行多重驗證,請使用這個方法。 
- 提供可略過的選項,讓使用者在註冊時註冊第二重驗證。如要鼓勵使用者採用多重驗證,但不強制要求使用者採用,則可能會偏好這種做法。 
- 提供從使用者帳戶或個人資料管理頁面新增第二因素的功能,而非在註冊畫面中提供。這樣一來,註冊程序的摩擦力會降到最低,同時仍可讓重視安全性的使用者使用多重驗證。 
- 當使用者想要存取需要更高安全性要求的功能時,要求逐步新增第二個因素。 
註冊次要驗證方式
如要為使用者註冊新的次要驗證方法,請按照下列步驟操作:
- 重新驗證使用者。 
- 請使用者輸入電話號碼。 
- 為使用者取得多重驗證工作階段: - Kotlin+KTX- user.multiFactor.session.addOnCompleteListener { task -> if (task.isSuccessful) { val multiFactorSession: MultiFactorSession? = task.result } }- Java- user.getMultiFactor().getSession() .addOnCompleteListener( new OnCompleteListener<MultiFactorSession>() { @Override public void onComplete(@NonNull Task<MultiFactorSession> task) { if (task.isSuccessful()) { MultiFactorSession multiFactorSession = task.getResult(); } } });
- 建構 - OnVerificationStateChangedCallbacks物件,以便在驗證程序中處理不同的事件:- Kotlin+KTX- val callbacks = object : OnVerificationStateChangedCallbacks() { override fun onVerificationCompleted(credential: PhoneAuthCredential) { // This callback will be invoked in two situations: // 1) Instant verification. In some cases, the phone number can be // instantly verified without needing to send or enter a verification // code. You can disable this feature by calling // PhoneAuthOptions.builder#requireSmsValidation(true) when building // the options to pass to PhoneAuthProvider#verifyPhoneNumber(). // 2) Auto-retrieval. On some devices, Google Play services can // automatically detect the incoming verification SMS and perform // verification without user action. this@MainActivity.credential = credential } override fun onVerificationFailed(e: FirebaseException) { // This callback is invoked in response to invalid requests for // verification, like an incorrect phone number. if (e is FirebaseAuthInvalidCredentialsException) { // Invalid request // ... } else if (e is FirebaseTooManyRequestsException) { // The SMS quota for the project has been exceeded // ... } // Show a message and update the UI // ... } override fun onCodeSent( verificationId: String, forceResendingToken: ForceResendingToken ) { // The SMS verification code has been sent to the provided phone number. // We now need to ask the user to enter the code and then construct a // credential by combining the code with a verification ID. // Save the verification ID and resending token for later use. this@MainActivity.verificationId = verificationId this@MainActivity.forceResendingToken = forceResendingToken // ... } }- Java- OnVerificationStateChangedCallbacks callbacks = new OnVerificationStateChangedCallbacks() { @Override public void onVerificationCompleted(PhoneAuthCredential credential) { // This callback will be invoked in two situations: // 1) Instant verification. In some cases, the phone number can be // instantly verified without needing to send or enter a verification // code. You can disable this feature by calling // PhoneAuthOptions.builder#requireSmsValidation(true) when building // the options to pass to PhoneAuthProvider#verifyPhoneNumber(). // 2) Auto-retrieval. On some devices, Google Play services can // automatically detect the incoming verification SMS and perform // verification without user action. this.credential = credential; } @Override public void onVerificationFailed(FirebaseException e) { // This callback is invoked in response to invalid requests for // verification, like an incorrect phone number. if (e instanceof FirebaseAuthInvalidCredentialsException) { // Invalid request // ... } else if (e instanceof FirebaseTooManyRequestsException) { // The SMS quota for the project has been exceeded // ... } // Show a message and update the UI // ... } @Override public void onCodeSent( String verificationId, PhoneAuthProvider.ForceResendingToken token) { // The SMS verification code has been sent to the provided phone number. // We now need to ask the user to enter the code and then construct a // credential by combining the code with a verification ID. // Save the verification ID and resending token for later use. this.verificationId = verificationId; this.forceResendingToken = token; // ... } };
- 使用使用者的電話號碼、多重驗證工作階段和回呼,初始化 - PhoneInfoOptions物件:- Kotlin+KTX- val phoneAuthOptions = PhoneAuthOptions.newBuilder() .setPhoneNumber(phoneNumber) .setTimeout(30L, TimeUnit.SECONDS) .setMultiFactorSession(MultiFactorSession) .setCallbacks(callbacks) .build()- Java- PhoneAuthOptions phoneAuthOptions = PhoneAuthOptions.newBuilder() .setPhoneNumber(phoneNumber) .setTimeout(30L, TimeUnit.SECONDS) .setMultiFactorSession(multiFactorSession) .setCallbacks(callbacks) .build();- 根據預設,系統會啟用即時驗證功能。如要停用,請新增對 - requireSmsValidation(true)的呼叫。
- 將驗證訊息傳送至使用者的手機: - Kotlin+KTX- PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions)- Java- PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);- 雖然這不是強制規定,但建議您事先告知使用者,他們將收到簡訊,並且會收取標準費率。 
- 傳送簡訊碼後,請要求使用者驗證這組碼: - Kotlin+KTX- // Ask user for the verification code. val credential = PhoneAuthProvider.getCredential(verificationId, verificationCode)- Java- // Ask user for the verification code. PhoneAuthCredential credential = PhoneAuthProvider.getCredential(verificationId, verificationCode);
- 使用 - PhoneAuthCredential初始化- MultiFactorAssertion物件:- Kotlin+KTX- val multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential)- Java- MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
- 完成註冊。您可以選擇為第二個因素指定顯示名稱。這對擁有多個次要驗證方法的使用者來說很有幫助,因為電話號碼會在驗證流程中遮蓋 (例如 +1******1234)。 - Kotlin+KTX- // Complete enrollment. This will update the underlying tokens // and trigger ID token change listener. FirebaseAuth.getInstance() .currentUser ?.multiFactor ?.enroll(multiFactorAssertion, "My personal phone number") ?.addOnCompleteListener { // ... }- Java- // Complete enrollment. This will update the underlying tokens // and trigger ID token change listener. FirebaseAuth.getInstance() .getCurrentUser() .getMultiFactor() .enroll(multiFactorAssertion, "My personal phone number") .addOnCompleteListener( new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { // ... } });
以下程式碼為註冊第二個因素的完整範例:
Kotlin+KTX
val multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential)
user.multiFactor.session
    .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            val multiFactorSession = task.result
            val phoneAuthOptions = PhoneAuthOptions.newBuilder()
                .setPhoneNumber(phoneNumber)
                .setTimeout(30L, TimeUnit.SECONDS)
                .setMultiFactorSession(multiFactorSession)
                .setCallbacks(callbacks)
                .build()
            // Send SMS verification code.
            PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions)
        }
    }
// Ask user for the verification code.
val credential = PhoneAuthProvider.getCredential(verificationId, verificationCode)
val multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential)
// Complete enrollment.
FirebaseAuth.getInstance()
    .currentUser
    ?.multiFactor
    ?.enroll(multiFactorAssertion, "My personal phone number")
    ?.addOnCompleteListener {
        // ...
    }
Java
MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
user.getMultiFactor().getSession()
  .addOnCompleteListener(
      new OnCompleteListener<MultiFactorSession>() {
      @Override
      public void onComplete(@NonNull Task<MultiFactorSession> task) {
        if (task.isSuccessful()) {
          MultiFactorSession multiFactorSession = task.getResult();
          PhoneAuthOptions phoneAuthOptions =
            PhoneAuthOptions.newBuilder()
                .setPhoneNumber(phoneNumber)
                .setTimeout(30L, TimeUnit.SECONDS)
                .setMultiFactorSession(multiFactorSession)
                .setCallbacks(callbacks)
                .build();
          // Send SMS verification code.
          PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
        }
      }
      });
// Ask user for the verification code.
PhoneAuthCredential credential =
  PhoneAuthProvider.getCredential(verificationId, verificationCode);
MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
// Complete enrollment.
FirebaseAuth.getInstance()
  .getCurrentUser()
  .getMultiFactor()
  .enroll(multiFactorAssertion, "My personal phone number")
  .addOnCompleteListener(
      new OnCompleteListener<Void>() {
      @Override
      public void onComplete(@NonNull Task<Void> task) {
        // ...
      }
      });
恭喜!您已為使用者成功註冊第二個驗證因素。
使用第二種驗證方法登入
如要使用簡訊雙重驗證登入使用者,請按照下列步驟操作:
- 請使用者以第一個因素登入,然後擷取 - FirebaseAuthMultiFactorException例外狀況。這個錯誤包含解析器,可用於取得使用者註冊的第二因素。它還包含底層工作階段,證明使用者已成功透過第一因素驗證。- 舉例來說,如果使用者的第一個因素是電子郵件和密碼: - Kotlin+KTX- FirebaseAuth.getInstance() .signInWithEmailAndPassword(email, password) .addOnCompleteListener( OnCompleteListener { task -> if (task.isSuccessful) { // User is not enrolled with a second factor and is successfully // signed in. // ... return@OnCompleteListener } if (task.exception is FirebaseAuthMultiFactorException) { // The user is a multi-factor user. Second factor challenge is // required. val multiFactorResolver = (task.exception as FirebaseAuthMultiFactorException).resolver // ... } else { // Handle other errors, such as wrong password. } })- Java- FirebaseAuth.getInstance() .signInWithEmailAndPassword(email, password) .addOnCompleteListener( new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { if (task.isSuccessful()) { // User is not enrolled with a second factor and is successfully // signed in. // ... return; } if (task.getException() instanceof FirebaseAuthMultiFactorException) { // The user is a multi-factor user. Second factor challenge is // required. MultiFactorResolver multiFactorResolver = task.getException().getResolver(); // ... } else { // Handle other errors such as wrong password. } } });- 如果使用者的第一個因素是聯合驗證機構 (例如 OAuth),請在呼叫 - startActivityForSignInWithProvider()後擷取錯誤。
- 如果使用者註冊了多個次要驗證因素,請詢問他們要使用哪一個: - Kotlin+KTX- // Ask user which second factor to use. // You can get the list of enrolled second factors using // multiFactorResolver.hints // Check the selected factor: if (multiFactorResolver.hints[selectedIndex].factorId === PhoneMultiFactorGenerator.FACTOR_ID ) { // User selected a phone second factor. val selectedHint = multiFactorResolver.hints[selectedIndex] as PhoneMultiFactorInfo } else if (multiFactorResolver.hints[selectedIndex].factorId === TotpMultiFactorGenerator.FACTOR_ID) { // User selected a TOTP second factor. } else { // Unsupported second factor. }- Java- // Ask user which second factor to use. // You can get the masked phone number using // resolver.getHints().get(selectedIndex).getPhoneNumber() // You can get the display name using // resolver.getHints().get(selectedIndex).getDisplayName() if ( resolver.getHints() .get(selectedIndex) .getFactorId() .equals( PhoneMultiFactorGenerator.FACTOR_ID ) ) { // User selected a phone second factor. MultiFactorInfo selectedHint = multiFactorResolver.getHints().get(selectedIndex); } else if ( resolver .getHints() .get(selectedIndex) .getFactorId() .equals(TotpMultiFactorGenerator.FACTOR_ID ) ) { // User selected a TOTP second factor. } else { // Unsupported second factor. }
- 使用提示和多因素工作階段初始化 - PhoneAuthOptions物件。這些值包含在附加至- FirebaseAuthMultiFactorException的解析器中。- Kotlin+KTX- val phoneAuthOptions = PhoneAuthOptions.newBuilder() .setMultiFactorHint(selectedHint) .setTimeout(30L, TimeUnit.SECONDS) .setMultiFactorSession(multiFactorResolver.session) .setCallbacks(callbacks) // Optionally disable instant verification. // .requireSmsValidation(true) .build()- Java- PhoneAuthOptions phoneAuthOptions = PhoneAuthOptions.newBuilder() .setMultiFactorHint(selectedHint) .setTimeout(30L, TimeUnit.SECONDS) .setMultiFactorSession(multiFactorResolver.getSession()) .setCallbacks(callbacks) // Optionally disable instant verification. // .requireSmsValidation(true) .build();
- 將驗證訊息傳送至使用者的手機: - Kotlin+KTX- // Send SMS verification code PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions)- Java- // Send SMS verification code PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
- 傳送簡訊碼後,請要求使用者驗證這組碼: - Kotlin+KTX- // Ask user for the verification code. Then, pass it to getCredential: val credential = PhoneAuthProvider.getCredential(verificationId, verificationCode)- Java- // Ask user for the verification code. Then, pass it to getCredential: PhoneAuthCredential credential = PhoneAuthProvider.getCredential(verificationId, verificationCode);
- 使用 - PhoneAuthCredential初始化- MultiFactorAssertion物件:- Kotlin+KTX- val multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential)- Java- MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
- 請呼叫 - resolver.resolveSignIn()完成次要驗證。接著,您可以存取原始登入結果,其中包含標準提供者專屬資料和驗證憑證:- Kotlin+KTX- multiFactorResolver .resolveSignIn(multiFactorAssertion) .addOnCompleteListener { task -> if (task.isSuccessful) { val authResult = task.result // AuthResult will also contain the user, additionalUserInfo, // and an optional credential (null for email/password) // associated with the first factor sign-in. // For example, if the user signed in with Google as a first // factor, authResult.getAdditionalUserInfo() will contain data // related to Google provider that the user signed in with; // authResult.getCredential() will contain the Google OAuth // credential; // authResult.getCredential().getAccessToken() will contain the // Google OAuth access token; // authResult.getCredential().getIdToken() contains the Google // OAuth ID token. } }- Java- multiFactorResolver .resolveSignIn(multiFactorAssertion) .addOnCompleteListener( new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { if (task.isSuccessful()) { AuthResult authResult = task.getResult(); // AuthResult will also contain the user, additionalUserInfo, // and an optional credential (null for email/password) // associated with the first factor sign-in. // For example, if the user signed in with Google as a first // factor, authResult.getAdditionalUserInfo() will contain data // related to Google provider that the user signed in with. // authResult.getCredential() will contain the Google OAuth // credential. // authResult.getCredential().getAccessToken() will contain the // Google OAuth access token. // authResult.getCredential().getIdToken() contains the Google // OAuth ID token. } } });
以下程式碼為多重驗證使用者登入的完整範例:
Kotlin+KTX
FirebaseAuth.getInstance()
    .signInWithEmailAndPassword(email, password)
    .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            // User is not enrolled with a second factor and is successfully
            // signed in.
            // ...
            return@addOnCompleteListener
        }
        if (task.exception is FirebaseAuthMultiFactorException) {
            val multiFactorResolver =
                (task.exception as FirebaseAuthMultiFactorException).resolver
            // Ask user which second factor to use. Then, get
            // the selected hint:
            val selectedHint =
                multiFactorResolver.hints[selectedIndex] as PhoneMultiFactorInfo
            // Send the SMS verification code.
            PhoneAuthProvider.verifyPhoneNumber(
                PhoneAuthOptions.newBuilder()
                    .setActivity(this)
                    .setMultiFactorSession(multiFactorResolver.session)
                    .setMultiFactorHint(selectedHint)
                    .setCallbacks(generateCallbacks())
                    .setTimeout(30L, TimeUnit.SECONDS)
                    .build()
            )
            // Ask user for the SMS verification code, then use it to get
            // a PhoneAuthCredential:
            val credential =
                PhoneAuthProvider.getCredential(verificationId, verificationCode)
            // Initialize a MultiFactorAssertion object with the
            // PhoneAuthCredential.
            val multiFactorAssertion: MultiFactorAssertion =
                PhoneMultiFactorGenerator.getAssertion(credential)
            // Complete sign-in.
            multiFactorResolver
                .resolveSignIn(multiFactorAssertion)
                .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                        // User successfully signed in with the
                        // second factor phone number.
                    }
                    // ...
                }
        } else {
            // Handle other errors such as wrong password.
        }
    }
Java
FirebaseAuth.getInstance()
  .signInWithEmailAndPassword(email, password)
  .addOnCompleteListener(
      new OnCompleteListener<AuthResult>() {
      @Override
      public void onComplete(@NonNull Task<AuthResult> task) {
        if (task.isSuccessful()) {
          // User is not enrolled with a second factor and is successfully
          // signed in.
          // ...
          return;
        }
        if (task.getException() instanceof FirebaseAuthMultiFactorException) {
          FirebaseAuthMultiFactorException e =
            (FirebaseAuthMultiFactorException) task.getException();
          MultiFactorResolver multiFactorResolver = e.getResolver();
          // Ask user which second factor to use.
          MultiFactorInfo selectedHint =
            multiFactorResolver.getHints().get(selectedIndex);
          // Send the SMS verification code.
          PhoneAuthProvider.verifyPhoneNumber(
            PhoneAuthOptions.newBuilder()
                .setActivity(this)
                .setMultiFactorSession(multiFactorResolver.getSession())
                .setMultiFactorHint(selectedHint)
                .setCallbacks(generateCallbacks())
                .setTimeout(30L, TimeUnit.SECONDS)
                .build());
          // Ask user for the SMS verification code.
          PhoneAuthCredential credential =
            PhoneAuthProvider.getCredential(verificationId, verificationCode);
          // Initialize a MultiFactorAssertion object with the
          // PhoneAuthCredential.
          MultiFactorAssertion multiFactorAssertion =
            PhoneMultiFactorGenerator.getAssertion(credential);
          // Complete sign-in.
          multiFactorResolver
            .resolveSignIn(multiFactorAssertion)
            .addOnCompleteListener(
                new OnCompleteListener<AuthResult>() {
                  @Override
                  public void onComplete(@NonNull Task<AuthResult> task) {
                  if (task.isSuccessful()) {
                    // User successfully signed in with the
                    // second factor phone number.
                  }
                  // ...
                  }
                });
        } else {
          // Handle other errors such as wrong password.
        }
      }
      });
恭喜!您已成功使用多重驗證登入使用者。
後續步驟
- 使用 Admin SDK 以程式輔助的方式管理多重驗證使用者。