앱의 민감한 정보나 고급 콘텐츠를 보호하는 데는 얼굴 인식, 지문 인식 같은 생체 인식 인증을 요청하는 방법이 있습니다. 이 가이드에서는 앱에서 생체 인식 로그인 흐름을 지원하는 방법을 설명합니다.
일반적으로 기기에서 처음 로그인할 때는 인증 관리자를 사용해야 합니다. 후속 재승인은 생체 인식 메시지 또는 인증 관리자를 사용하여 수행할 수 있습니다. 생체 인식 메시지를 사용하면 맞춤설정 옵션이 더 많다는 이점이 있지만 인증 관리자는 두 흐름 모두에서 단일 구현을 제공합니다.
앱에서 지원하는 인증 유형 선언
앱에서 지원하는 인증 유형을 정의하려면 BiometricManager.Authenticators
인터페이스를 사용합니다. 시스템에서 다음 유형의 인증을 선언할 수 있습니다.
BIOMETRIC_STRONG
- Android 호환성 정의 페이지에서 정의한 클래스 3 생체 인식을 사용하는 인증
BIOMETRIC_WEAK
- Android 호환성 정의 페이지에서 정의한 클래스 2 생체 인식을 사용하는 인증
DEVICE_CREDENTIAL
- 화면 잠금 사용자 인증 정보를 사용하는 인증 - 사용자의 PIN, 패턴 또는 비밀번호
인증자를 사용하려면 사용자가 PIN, 패턴 또는 비밀번호를 만들어야 합니다. 아직 사용자가 만들지 않았다면 생체 인식 등록 흐름에서 사용자에게 만들라는 메시지를 표시합니다.
앱에서 허용하는 생체 인식 인증 유형을 정의하려면 인증 유형 또는 비트별 유형 조합을 setAllowedAuthenticators()
메서드에 전달합니다. 다음 코드 스니펫은 클래스 3 생체 인식이나 화면 잠금 사용자 인증 정보를 사용하여 인증을 지원하는 방법을 보여 줍니다.
Kotlin
// Lets the user authenticate using either a Class 3 biometric or // their lock screen credential (PIN, pattern, or password). promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) .build()
자바
// Lets user authenticate using either a Class 3 biometric or // their lock screen credential (PIN, pattern, or password). promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) .build();
DEVICE_CREDENTIAL
및 BIOMETRIC_STRONG | DEVICE_CREDENTIAL
인증자 유형 조합은 Android 10(API 수준 29) 이하에서 지원되지 않습니다. Android 10 이하에서 PIN이나 패턴, 비밀번호가 있는지 확인하려면 KeyguardManager.isDeviceSecure()
메서드를 사용하세요.
생체 인식 인증을 사용할 수 있는지 확인
앱에서 지원하는 인증 요소를 결정했으면 이러한 요소가 실제로 사용 가능한지 확인합니다. 이렇게 하려면 setAllowedAuthenticators()
메서드를 사용하여 선언한 동일한 비트별 유형 조합을 canAuthenticate()
메서드에 전달합니다.
필요한 경우 ACTION_BIOMETRIC_ENROLL
인텐트 작업을 호출합니다. 추가 인텐트에 앱에서 허용하는 인증자 세트를 제공합니다. 이 인텐트는 사용자에게 앱에서 허용하는 인증자의 사용자 인증 정보를 등록하라는 메시지를 표시합니다.
Kotlin
val biometricManager = BiometricManager.from(this) when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) { BiometricManager.BIOMETRIC_SUCCESS -> Log.d("MY_APP_TAG", "App can authenticate using biometrics.") BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> Log.e("MY_APP_TAG", "No biometric features available on this device.") BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> Log.e("MY_APP_TAG", "Biometric features are currently unavailable.") BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { // Prompts the user to create credentials that your app accepts. val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply { putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BIOMETRIC_STRONG or DEVICE_CREDENTIAL) } startActivityForResult(enrollIntent, REQUEST_CODE) } }
Java
BiometricManager biometricManager = BiometricManager.from(this); switch (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)) { case BiometricManager.BIOMETRIC_SUCCESS: Log.d("MY_APP_TAG", "App can authenticate using biometrics."); break; case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE: Log.e("MY_APP_TAG", "No biometric features available on this device."); break; case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE: Log.e("MY_APP_TAG", "Biometric features are currently unavailable."); break; case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED: // Prompts the user to create credentials that your app accepts. final Intent enrollIntent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL); enrollIntent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BIOMETRIC_STRONG | DEVICE_CREDENTIAL); startActivityForResult(enrollIntent, REQUEST_CODE); break; }
사용자 인증 방식 확인
사용자가 인증한 이후에는 getAuthenticationType()
을 호출함으로써 기기 사용자 인증 정보와 생체 인식 사용자 인증 정보 중 어떤 정보가 사용되었는지 확인할 수 있습니다.
로그인 메시지 표시
사용자에게 생체 인식 사용자 인증 정보를 사용하여 인증하도록 요청하는 시스템 메시지를 표시하려면 생체 인식 라이브러리를 사용하세요. 시스템에서 제공하는 이 대화상자는 라이브러리를 사용하는 앱 전체에서 일관성이 유지되므로 더 신뢰할 수 있는 사용자 환경이 구성됩니다. 대화상자 예는 그림 1에서 확인할 수 있습니다.
생체 인식 라이브러리를 사용하여 앱에 생체 인식 인증을 추가하려면 다음 단계를 완료하세요.
앱 모듈의
build.gradle
파일에서androidx.biometric
라이브러리의 종속 항목을 추가합니다.생체 인식 로그인 대화상자를 호스팅하는 활동 또는 프래그먼트에서 다음 코드 스니펫과 같은 로직을 사용하여 대화상자를 표시합니다.
Kotlin
private lateinit var executor: Executor private lateinit var biometricPrompt: BiometricPrompt private lateinit var promptInfo: BiometricPrompt.PromptInfo override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) executor = ContextCompat.getMainExecutor(this) biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) Toast.makeText(applicationContext, "Authentication error: $errString", Toast.LENGTH_SHORT) .show() } override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) Toast.makeText(applicationContext, "Authentication succeeded!", Toast.LENGTH_SHORT) .show() } override fun onAuthenticationFailed() { super.onAuthenticationFailed() Toast.makeText(applicationContext, "Authentication failed", Toast.LENGTH_SHORT) .show() } }) promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Use account password") .build() // Prompt appears when user clicks "Log in". // Consider integrating with the keystore to unlock cryptographic operations, // if needed by your app. val biometricLoginButton = findViewById<Button>(R.id.biometric_login) biometricLoginButton.setOnClickListener { biometricPrompt.authenticate(promptInfo) } }
Java
private Executor executor; private BiometricPrompt biometricPrompt; private BiometricPrompt.PromptInfo promptInfo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); executor = ContextCompat.getMainExecutor(this); biometricPrompt = new BiometricPrompt(MainActivity.this, executor, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { super.onAuthenticationError(errorCode, errString); Toast.makeText(getApplicationContext(), "Authentication error: " + errString, Toast.LENGTH_SHORT) .show(); } @Override public void onAuthenticationSucceeded( @NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); Toast.makeText(getApplicationContext(), "Authentication succeeded!", Toast.LENGTH_SHORT).show(); } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); Toast.makeText(getApplicationContext(), "Authentication failed", Toast.LENGTH_SHORT) .show(); } }); promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Use account password") .build(); // Prompt appears when user clicks "Log in". // Consider integrating with the keystore to unlock cryptographic operations, // if needed by your app. Button biometricLoginButton = findViewById(R.id.biometric_login); biometricLoginButton.setOnClickListener(view -> { biometricPrompt.authenticate(promptInfo); }); }
인증을 사용하는 암호화 솔루션 이용
앱 내에서 민감한 정보를 더욱 보호하려면 CryptoObject
의 인스턴스를 사용하여 암호화를 생체 인식 인증 워크플로에 통합하면 됩니다.
프레임워크는 Signature
, Cipher
, Mac
과 같은 암호화 객체를 지원합니다.
사용자가 생체 인식 메시지를 사용하여 인증되면 앱은 암호화 작업을 수행할 수 있습니다. 예를 들어 개발자가 Cipher
객체를 사용하여 인증되면 앱은 SecretKey
객체를 사용하여 암호화 및 복호화를 진행할 수 있습니다.
다음 섹션에서는 Cipher
객체와 SecretKey
객체를 사용하여 데이터를 암호화하는 예를 살펴봅니다. 각 예에서는 다음 메서드가 사용됩니다.
Kotlin
private fun generateSecretKey(keyGenParameterSpec: KeyGenParameterSpec) { val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey() } private fun getSecretKey(): SecretKey { val keyStore = KeyStore.getInstance("AndroidKeyStore") // Before the keystore can be accessed, it must be loaded. keyStore.load(null) return keyStore.getKey(KEY_NAME, null) as SecretKey } private fun getCipher(): Cipher { return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7) }
Java
private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) { KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); keyGenerator.init(keyGenParameterSpec); keyGenerator.generateKey(); } private SecretKey getSecretKey() { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); // Before the keystore can be accessed, it must be loaded. keyStore.load(null); return ((SecretKey)keyStore.getKey(KEY_NAME, null)); } private Cipher getCipher() { return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); }
생체 인식 사용자 인증 정보만 사용하여 인증
잠금 해제할 때 앱에서 생체 인식 사용자 인증 정보를 요구하는 보안 비밀 키를 사용하는 경우 사용자가 매번 생체 인식 사용자 인증 정보를 인증해야 앱이 키에 액세스할 수 있습니다.
사용자가 생체 인식 사용자 인증 정보를 사용하여 인증한 후에만 민감한 정보를 암호화하려면 다음 단계를 완료하세요.
다음
KeyGenParameterSpec
구성을 사용하는 키를 생성합니다.Kotlin
generateSecretKey(KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) // Invalidate the keys if the user has registered a new biometric // credential, such as a new fingerprint. Can call this method only // on Android 7.0 (API level 24) or higher. The variable // "invalidatedByBiometricEnrollment" is true by default. .setInvalidatedByBiometricEnrollment(true) .build())
자바
generateSecretKey(new KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) // Invalidate the keys if the user has registered a new biometric // credential, such as a new fingerprint. Can call this method only // on Android 7.0 (API level 24) or higher. The variable // "invalidatedByBiometricEnrollment" is true by default. .setInvalidatedByBiometricEnrollment(true) .build());
암호화를 통합하는 생체 인식 인증 워크플로를 시작합니다.
Kotlin
biometricLoginButton.setOnClickListener { // Exceptions are unhandled within this snippet. val cipher = getCipher() val secretKey = getSecretKey() cipher.init(Cipher.ENCRYPT_MODE, secretKey) biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) }
자바
biometricLoginButton.setOnClickListener(view -> { // Exceptions are unhandled within this snippet. Cipher cipher = getCipher(); SecretKey secretKey = getSecretKey(); cipher.init(Cipher.ENCRYPT_MODE, secretKey); biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipher)); });
생체 인식 인증 콜백 내에서 다음과 같이 보안 비밀 키를 사용하여 민감한 정보를 암호화합니다.
Kotlin
override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult) { val encryptedInfo: ByteArray = result.cryptoObject.cipher?.doFinal( // plaintext-string text is whatever data the developer would like // to encrypt. It happens to be plain-text in this example, but it // can be anything plaintext-string.toByteArray(Charset.defaultCharset()) ) Log.d("MY_APP_TAG", "Encrypted information: " + Arrays.toString(encryptedInfo)) }
Java
@Override public void onAuthenticationSucceeded( @NonNull BiometricPrompt.AuthenticationResult result) { // NullPointerException is unhandled; use Objects.requireNonNull(). byte[] encryptedInfo = result.getCryptoObject().getCipher().doFinal( // plaintext-string text is whatever data the developer would like // to encrypt. It happens to be plain-text in this example, but it // can be anything plaintext-string.getBytes(Charset.defaultCharset())); Log.d("MY_APP_TAG", "Encrypted information: " + Arrays.toString(encryptedInfo)); }
생체 인식 또는 잠금 화면 사용자 인증 정보를 사용하여 인증
보안 비밀 키를 사용하여, 생체 인식 사용자 인증 정보 또는 잠금 화면 사용자 인증 정보(PIN, 패턴 또는 비밀번호)를 사용한 인증을 허용할 수 있습니다. 이 키를 구성할 때 유효기간을 지정합니다. 이 기간에 앱은 사용자의 재인증 없이 여러 암호화 작업을 할 수 있습니다.
사용자가 생체 인식 사용자 인증 정보 또는 잠금 화면 사용자 인증 정보를 사용하여 인증한 후에만 민감한 정보를 암호화하려면 다음 단계를 완료하세요.
다음
KeyGenParameterSpec
구성을 사용하는 키를 생성합니다.Kotlin
generateSecretKey(KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS, ALLOWED_AUTHENTICATORS) .build())
자바
generateSecretKey(new KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS, ALLOWED_AUTHENTICATORS) .build());
사용자 인증 후
VALIDITY_DURATION_SECONDS
시간 이내에 다음과 같이 민감한 정보를 암호화합니다.Kotlin
private fun encryptSecretInformation() { // Exceptions are unhandled for getCipher() and getSecretKey(). val cipher = getCipher() val secretKey = getSecretKey() try { cipher.init(Cipher.ENCRYPT_MODE, secretKey) val encryptedInfo: ByteArray = cipher.doFinal( // plaintext-string text is whatever data the developer would // like to encrypt. It happens to be plain-text in this example, // but it can be anything plaintext-string.toByteArray(Charset.defaultCharset())) Log.d("MY_APP_TAG", "Encrypted information: " + Arrays.toString(encryptedInfo)) } catch (e: InvalidKeyException) { Log.e("MY_APP_TAG", "Key is invalid.") } catch (e: UserNotAuthenticatedException) { Log.d("MY_APP_TAG", "The key's validity timed out.") biometricPrompt.authenticate(promptInfo) }
Java
private void encryptSecretInformation() { // Exceptions are unhandled for getCipher() and getSecretKey(). Cipher cipher = getCipher(); SecretKey secretKey = getSecretKey(); try { // NullPointerException is unhandled; use Objects.requireNonNull(). ciper.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedInfo = cipher.doFinal( // plaintext-string text is whatever data the developer would // like to encrypt. It happens to be plain-text in this example, // but it can be anything plaintext-string.getBytes(Charset.defaultCharset())); } catch (InvalidKeyException e) { Log.e("MY_APP_TAG", "Key is invalid."); } catch (UserNotAuthenticatedException e) { Log.d("MY_APP_TAG", "The key's validity timed out."); biometricPrompt.authenticate(promptInfo); } }
사용당 인증 키를 사용한 인증
BiometricPrompt
인스턴스 내에서 사용당 인증 키를 지원할 수 있습니다. 이러한 키를 사용하려면 앱이 키로 보호되는 데이터에 액세스해야 할 때마다 사용자가 생체 인식 사용자 인증 정보 또는 기기 사용자 인증 정보를 제시해야 합니다. 사용당 인증 키는 많은 금액을 결제하거나 개인의 건강 기록을 업데이트하는 등의 중요한 트랜잭션에 유용할 수 있습니다.
BiometricPrompt
객체를 사용당 인증 키와 연결하려면 다음과 비슷한 코드를 추가합니다.
Kotlin
val authPerOpKeyGenParameterSpec = KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose) // Accept either a biometric credential or a device credential. // To accept only one type of credential, include only that type as the // second argument. .setUserAuthenticationParameters(0 /* duration */, KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL) .build()
Java
KeyGenParameterSpec authPerOpKeyGenParameterSpec = new KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose) // Accept either a biometric credential or a device credential. // To accept only one type of credential, include only that type as the // second argument. .setUserAuthenticationParameters(0 /* duration */, KeyProperties.AUTH_BIOMETRIC_STRONG | KeyProperties.AUTH_DEVICE_CREDENTIAL) .build();
명시적인 사용자 작업 없이 인증하기
기본적으로 시스템에서는 사용자의 생체 인식 사용자 인증 정보 수락 전에 버튼 누르기와 같은 특정 작업을 하도록 사용자에게 요구합니다. 이 구성은 앱이 민감하거나 위험성이 높은 작업(예: 구매)을 확인하는 대화상자를 표시하는 경우 권장됩니다.
그러나 앱이 위험성이 낮은 작업과 관련하여 생체 인식 인증 대화상자를 표시하는 경우에는 사용자에게 인증을 확인할 필요가 없다는 것을 알리는 힌트를 시스템에 제공할 수 있습니다. 이 힌트를 통해 사용자는 수동적 모달리티(예: 얼굴 또는 홍채 기반 인식)를 사용한 재인증 후에 더 신속하게 앱의 콘텐츠를 볼 수 있습니다. 이 힌트를 제공하려면 false
를 setConfirmationRequired()
메서드에 전달합니다.
그림 2에서는 같은 대화상자의 두 버전을 보여줍니다. 한 버전은 명시적인 사용자 작업을 요구하고, 다른 한 버전은 요구하지 않습니다.
다음 코드 스니펫은 인증 프로세스를 완료하기 위해 명시적인 사용자 작업을 요구하지 않는 대화상자를 제시하는 방법을 보여줍니다.
Kotlin
// Lets the user authenticate without performing an action, such as pressing a // button, after their biometric credential is accepted. promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Use account password") .setConfirmationRequired(false) .build()
Java
// Lets the user authenticate without performing an action, such as pressing a // button, after their biometric credential is accepted. promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Use account password") .setConfirmationRequired(false) .build();
생체 인식 이외의 사용자 인증 정보로의 대체 허용
생체 인식 사용자 인증 정보 또는 기기 사용자 인증 정보를 사용한 인증을 앱에 허용하려면 setAllowedAuthenticators()
에 전달하는 값 집합에 DEVICE_CREDENTIAL
을 포함하여 앱이 기기 사용자 인증 정보를 지원한다고 선언하면 됩니다.
앱에서 현재 createConfirmDeviceCredentialIntent()
또는 setDeviceCredentialAllowed()
를 사용하여 이 기능을 제공하는 경우 setAllowedAuthenticators()
를 사용하는 것으로 전환해야 합니다.
추가 리소스
Android의 생체 인식 인증에 관해 자세히 알아보려면 다음 리소스를 참고하세요.