๐ Local Authentication ์์ ์ ๋ณต
Face ID์ Touch ID๋ก ์ฑ์ ์์ ํ๊ฒ! ์์ฒด ์ธ์ฆ๋ถํฐ ๋น๋ฐ๋ฒํธ ๋์ฒด๊น์ง ์๋ฒฝ ๊ฐ์ด๋.
โจ Local Authentication์ด๋?
Local Authentication์ Face ID, Touch ID, ๊ธฐ๊ธฐ ์ํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๋ ํ๋ ์์ํฌ์ ๋๋ค. ๋ก๊ทธ์ธ, ๊ฒฐ์ ์น์ธ, ๋ฏผ๊ฐํ ์ ๋ณด ์ ๊ทผ ์ ์์ ํ๊ณ ํธ๋ฆฌํ ์ธ์ฆ์ ์ ๊ณตํฉ๋๋ค. ๋ชจ๋ ์์ฒด ๋ฐ์ดํฐ๋ ๊ธฐ๊ธฐ ๋ด์์๋ง ์ฒ๋ฆฌ๋์ด ์๋ฒ๋ก ์ ์ก๋์ง ์์ต๋๋ค.
๐ฆ ์ฃผ์ ๊ธฐ๋ฅ
๊ธฐ๋ฅ ๊ฐ์
โ
Face ID / Touch ID ์ธ์ฆ
โ
๊ธฐ๊ธฐ ์ํธ ์ธ์ฆ (๋น๋ฐ๋ฒํธ)
โ
์์ฒด ์ธ์ฆ ๊ฐ์ฉ์ฑ ํ์ธ
โ
์ธ์ฆ ์คํจ ์ฒ๋ฆฌ ๋ฐ ์ฌ์๋
โ
์ปค์คํ
์ธ์ฆ UI
โ
๋น๋ฐ๋ฒํธ ๋์ฒด ์ธ์ฆ๐ ์์ฒด ์ธ์ฆ ๊ฐ์ฉ์ฑ ํ์ธ
BiometryCheck.swift
import LocalAuthentication func checkBiometryAvailability() -> (Bool, String) { let context = LAContext() var error: NSError? // ์์ฒด ์ธ์ฆ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ let canEvaluate = context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) if canEvaluate { // ์์ฒด ์ธ์ฆ ํ์ ํ์ธ switch context.biometryType { case .faceID: return (true, "Face ID ์ฌ์ฉ ๊ฐ๋ฅ") case .touchID: return (true, "Touch ID ์ฌ์ฉ ๊ฐ๋ฅ") case .opticID: return (true, "Optic ID ์ฌ์ฉ ๊ฐ๋ฅ") case .none: return (false, "์์ฒด ์ธ์ฆ ๋ฏธ์ง์") @unknown default: return (false, "์ ์ ์๋ ํ์ ") } } else { // ์ค๋ฅ ์ฒ๋ฆฌ if let error = error { switch LAError.Code(rawValue: error.code) { case .biometryNotEnrolled: return (false, "์์ฒด ์ธ์ฆ ๋ฏธ๋ฑ๋ก") case .biometryNotAvailable: return (false, "์์ฒด ์ธ์ฆ ๋ฏธ์ง์") case .passcodeNotSet: return (false, "๊ธฐ๊ธฐ ์ํธ ๋ฏธ์ค์ ") default: return (false, error.localizedDescription) } } return (false, "์ ์ ์๋ ์ค๋ฅ") } } // ์ฌ์ฉ ์์ let (available, message) = checkBiometryAvailability() print(message) // "Face ID ์ฌ์ฉ ๊ฐ๋ฅ"
๐ ๊ธฐ๋ณธ ์์ฒด ์ธ์ฆ
BasicAuth.swift
import LocalAuthentication func authenticateUser() async -> Bool { let context = LAContext() var error: NSError? // ์์ฒด ์ธ์ฆ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ guard context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) else { print("์์ฒด ์ธ์ฆ ๋ถ๊ฐ: \(error?.localizedDescription ?? "")") return false } do { // ์ธ์ฆ ์๋ let success = try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "์ฑ์ ๋ก๊ทธ์ธํ๋ ค๋ฉด ์ธ์ฆ์ด ํ์ํฉ๋๋ค" ) return success } catch let error { // ์ธ์ฆ ์คํจ ์ฒ๋ฆฌ if let laError = error as? LAError { switch laError.code { case .authenticationFailed: print("์ธ์ฆ ์คํจ (์ผ๊ตด/์ง๋ฌธ ๋ถ์ผ์น)") case .userCancel: print("์ฌ์ฉ์๊ฐ ์ทจ์ํจ") case .userFallback: print("๋น๋ฐ๋ฒํธ ์ ๋ ฅ ์ ํ") case .systemCancel: print("์์คํ ์ด ์ทจ์ (์ฑ ์ ํ ๋ฑ)") case .biometryLockout: print("๋๋ฌด ๋ง์ ์๋๋ก ์ ๊น") default: print("์ธ์ฆ ์ค๋ฅ: \(laError.localizedDescription)") } } return false } } // ์ฌ์ฉ ์์ let authenticated = await authenticateUser() if authenticated { print("๋ก๊ทธ์ธ ์ฑ๊ณต!") }
๐ ๊ธฐ๊ธฐ ์ํธ ํฌํจ ์ธ์ฆ
PasscodeAuth.swift
import LocalAuthentication func authenticateWithPasscode() async -> Bool { let context = LAContext() // ์์ฒด ์ธ์ฆ ์คํจ ์ ๊ธฐ๊ธฐ ์ํธ ์ ๋ ฅ ํ์ฉ context.localizedFallbackTitle = "์ํธ ์ ๋ ฅ" do { let success = try await context.evaluatePolicy( .deviceOwnerAuthentication, // ์์ฒด ์ธ์ฆ + ๊ธฐ๊ธฐ ์ํธ localizedReason: "๊ณ์ ์ ์ ๊ทผํ๋ ค๋ฉด ์ธ์ฆํ์ธ์" ) return success } catch { print("์ธ์ฆ ์คํจ: \(error.localizedDescription)") return false } } // ์ปค์คํ ์ทจ์ ๋ฒํผ ํ ์คํธ func authenticateWithCustomButton() async -> Bool { let context = LAContext() context.localizedCancelTitle = "๋์ค์" context.localizedFallbackTitle = "๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธ" do { return try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "๋ฏผ๊ฐํ ์ ๋ณด์ ์ ๊ทผํฉ๋๋ค" ) } catch { return false } }
๐ฑ SwiftUI ํตํฉ
AuthView.swift
import SwiftUI import LocalAuthentication struct AuthenticationView: View { @State private var isAuthenticated = false @State private var showError = false @State private var errorMessage = "" var body: some View { VStack(spacing: 20) { if isAuthenticated { // ์ธ์ฆ ํ ํ๋ฉด VStack(spacing: 16) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 80)) .foregroundStyle(.green) Text("์ธ์ฆ ์ฑ๊ณต") .font(.title) .bold() Text("์์ ํ๊ฒ ๋ก๊ทธ์ธ๋์์ต๋๋ค") .foregroundStyle(.secondary) Button("๋ก๊ทธ์์") { isAuthenticated = false } .buttonStyle(.borderedProminent) } } else { // ๋ก๊ทธ์ธ ํ๋ฉด VStack(spacing: 16) { Image(systemName: "faceid") .font(.system(size: 80)) .foregroundStyle(.blue) Text("์์ฒด ์ธ์ฆ ํ์") .font(.title) .bold() Text("Face ID๋ก ์์ ํ๊ฒ ๋ก๊ทธ์ธํ์ธ์") .foregroundStyle(.secondary) Button("์ธ์ฆํ๊ธฐ") { Task { await authenticate() } } .buttonStyle(.borderedProminent) } } } .padding() .alert("์ธ์ฆ ์คํจ", isPresented: $showError) { Button("ํ์ธ", role: .cancel) { } } message: { Text(errorMessage) } .onAppear { // ์ฑ ์์ ์ ์๋ ์ธ์ฆ ์๋ Task { await authenticate() } } } func authenticate() async { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy( .deviceOwnerAuthentication, error: &error ) else { errorMessage = error?.localizedDescription ?? "์ธ์ฆ ๋ถ๊ฐ" showError = true return } do { let success = try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: "์ฑ ์ ๊ทผ์ ์ํด ์ธ์ฆ์ด ํ์ํฉ๋๋ค" ) await MainActor.run { isAuthenticated = success } } catch let error { await MainActor.run { errorMessage = error.localizedDescription showError = true } } } }
๐ ์ค์ ์์ : ๋ฏผ๊ฐ ์ ๋ณด ๋ณดํธ
SecureDataView.swift
import SwiftUI import LocalAuthentication struct SecureDataView: View { @State private var isUnlocked = false @State private var sensitiveData = "****-****-****-1234" var body: some View { List { Section("๊ณ์ข ์ ๋ณด") { HStack { Text("๊ณ์ข๋ฒํธ") Spacer() Text(isUnlocked ? "1234-5678-9012-3456" : "****-****-****-3456") .font(.system(.body, design: .monospaced)) } HStack { Text("์์ก") Spacer() Text(isUnlocked ? "1,234,567์" : "*******์") } } Section { Button(isUnlocked ? "์ ๋ณด ์จ๊ธฐ๊ธฐ" : "์ ๋ณด ๋ณด๊ธฐ") { if isUnlocked { isUnlocked = false } else { Task { await authenticate() } } } } } .navigationTitle("๋ด ๊ณ์ข") } func authenticate() async { let context = LAContext() do { let success = try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: "๋ฏผ๊ฐํ ๊ณ์ข ์ ๋ณด๋ฅผ ๋ณด๋ ค๋ฉด ์ธ์ฆํ์ธ์" ) if success { await MainActor.run { withAnimation { isUnlocked = true } } } } catch { print("์ธ์ฆ ์คํจ") } } }
โฑ๏ธ ์ธ์ฆ ์ปจํ ์คํธ ์ฌ์ฌ์ฉ
ContextReuse.swift
import LocalAuthentication class AuthenticationManager { private let context = LAContext() init() { // ์ธ์ฆ ์ ํจ ์๊ฐ ์ค์ (๊ธฐ๋ณธ 10์ด) context.touchIDAuthenticationAllowableReuseDuration = 30 // 30์ด } func authenticate(reason: String) async -> Bool { do { // 30์ด ๋ด ์ฌ์ธ์ฆ ์ ์์ฒด ์ธ์ฆ ์๋ต return try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: reason ) } catch { return false } } func invalidate() { // ์ปจํ ์คํธ ๋ฌดํจํ (์ฆ์ ์ฌ์ธ์ฆ ํ์) context.invalidate() } } // ์ฌ์ฉ ์์ let authManager = AuthenticationManager() // ์ฒซ ์ธ์ฆ (์์ฒด ์ธ์ฆ ํ์) let success1 = await authManager.authenticate(reason: "๊ฒฐ์ ์น์ธ") // 30์ด ๋ด ์ฌ์ธ์ฆ (์์ฒด ์ธ์ฆ ์๋ต) let success2 = await authManager.authenticate(reason: "์ถ๊ฐ ๊ฒฐ์ ") // ๋ก๊ทธ์์ ์ ์ปจํ ์คํธ ๋ฌดํจํ authManager.invalidate()
๐ฏ ๊ณ ๊ธ ๊ธฐ๋ฅ: ๋๋ฉ์ธ ์ํ
DomainState.swift
import LocalAuthentication class BiometricStateMonitor { private var domainState: Data? func saveBiometricState() { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) else { return } // ํ์ฌ ์์ฒด ์ธ์ฆ ์ํ ์ ์ฅ domainState = context.evaluatedPolicyDomainState UserDefaults.standard.set(domainState, forKey: "BiometricState") } func hasBiometricChanged() -> Bool { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) else { return true } // ์ ์ฅ๋ ์ํ์ ๋น๊ต guard let savedState = UserDefaults.standard.data(forKey: "BiometricState") else { return true } let currentState = context.evaluatedPolicyDomainState return savedState != currentState } func handleBiometricChange() { if hasBiometricChanged() { print("โ ๏ธ ์์ฒด ์ธ์ฆ ์ ๋ณด๊ฐ ๋ณ๊ฒฝ๋์์ต๋๋ค!") print("๋ณด์์ ์ํด ๋ค์ ๋ก๊ทธ์ธํ์ธ์.") // ์ฑ ๋ฐ์ดํฐ ์ด๊ธฐํ ๋๋ ์ฌ๋ก๊ทธ์ธ ์๊ตฌ logoutUser() saveBiometricState() } } func logoutUser() { // ๋ก๊ทธ์์ ๋ก์ง UserDefaults.standard.set(false, forKey: "IsLoggedIn") } } // ์ฌ์ฉ ์์ let monitor = BiometricStateMonitor() monitor.saveBiometricState() // ๋ก๊ทธ์ธ ์ // ์ฑ ์์ ์ ํ์ธ monitor.handleBiometricChange()
๐ก HIG ๊ฐ์ด๋๋ผ์ธ
HIG ๊ถ์ฅ์ฌํญ
โ
DO
1. ๋ช
ํํ ์ด์ ์ ๊ณต
- "๊ฒฐ์ ๋ฅผ ์น์ธํ๋ ค๋ฉด Face ID๊ฐ ํ์ํฉ๋๋ค"
- ์ฌ์ฉ์์๊ฒ ์ ์ธ์ฆ์ด ํ์ํ์ง ๋ช
ํํ ์ค๋ช
2. ๋์ฒด ์๋จ ์ ๊ณต
- ์์ฒด ์ธ์ฆ ์คํจ ์ ๊ธฐ๊ธฐ ์ํธ ํ์ฉ
- .deviceOwnerAuthentication ์ ์ฑ
์ฌ์ฉ
3. ์ ์ ํ ํ์ด๋ฐ
- ๋ฏผ๊ฐํ ์์
์ง์ ์๋ง ์์ฒญ
- ๋๋ฌด ์์ฃผ ์์ฒญํ์ง ์๊ธฐ
4. ์ค๋ฅ ์ฒ๋ฆฌ
- ์ธ์ฆ ์คํจ ์ ๋ช
ํํ ํผ๋๋ฐฑ
- ์ฌ์๋ ์ต์
์ ๊ณต
โ DON'T
1. ์ฑ ์์๋ง๋ค ์ธ์ฆ ์๊ตฌ (๋ถํ์ํ ๊ฒฝ์ฐ)
2. ์์ฒด ๋ฐ์ดํฐ๋ฅผ ์๋ฒ๋ก ์ ์ก
3. ์ธ์ฆ ์์ด๋ ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ
4. "Touch ID"๋ผ๊ณ ๋ช
์ (๊ธฐ๊ธฐ๋ณ๋ก ๋ค๋ฆ)๐ง ์ค๋ฌด ํ์ฉ ํ
์ค์ ํจํด
import LocalAuthentication // 1. ์ธ์ฆ ํฌํผ ํด๋์ค class BiometricAuth { static let shared = BiometricAuth() private init() {} var biometricType: LABiometryType { let context = LAContext() _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) return context.biometryType } var biometricName: String { switch biometricType { case .faceID: return "Face ID" case .touchID: return "Touch ID" case .opticID: return "Optic ID" case .none: return "์์ฒด ์ธ์ฆ" @unknown default: return "์์ฒด ์ธ์ฆ" } } func authenticate(reason: String) async throws -> Bool { let context = LAContext() return try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: reason ) } } // 2. SwiftUI์์ ์ฌ์ฉ struct SettingsView: View { @AppStorage("useBiometric") private var useBiometric = false var body: some View { Form { Section("๋ณด์") { Toggle("\(BiometricAuth.shared.biometricName) ์ฌ์ฉ", isOn: $useBiometric) } } } } // 3. ๊ฒฐ์ ์น์ธ ์์ func processPayment(amount: Int) async -> Bool { do { let authenticated = try await BiometricAuth.shared.authenticate( reason: "\(amount)์ ๊ฒฐ์ ๋ฅผ ์น์ธํ์๊ฒ ์ต๋๊น?" ) if authenticated { // ๊ฒฐ์ ์ฒ๋ฆฌ await performPaymentTransaction(amount) return true } } catch { print("๊ฒฐ์ ์ทจ์๋จ") } return false } func performPaymentTransaction(_ amount: Int) async { // ์ค์ ๊ฒฐ์ API ํธ์ถ }
๐ Info.plist ์ค์
Info.plist
<!-- Face ID ์ฌ์ฉ ์ ํ์ --> <key>NSFaceIDUsageDescription</key> <string>์์ ํ ๋ก๊ทธ์ธ์ ์ํด Face ID๋ฅผ ์ฌ์ฉํฉ๋๋ค</string> <!-- ๋๋ ํ๊ตญ์ด๋ก --> <key>NSFaceIDUsageDescription</key> <string>๊ณ์ ๋ณด์์ ์ํด Face ID ์ธ์ฆ์ด ํ์ํฉ๋๋ค</string>
๐ก Local Authentication ํต์ฌ
โ
๋ชจ๋ ์์ฒด ๋ฐ์ดํฐ๋ ๊ธฐ๊ธฐ ๋ด์์๋ง ์ฒ๋ฆฌ
โ
Secure Enclave์ ์์ ํ๊ฒ ์ ์ฅ
โ
์์ฒด ์ธ์ฆ ์คํจ ์ ๊ธฐ๊ธฐ ์ํธ ๋์ฒด ์๋จ ์ ๊ณต
โ
์ฑ์ ์ธ์ฆ ์ฑ๊ณต/์คํจ๋ง ํ์ธ ๊ฐ๋ฅ
โ
์ค์ ์์ฒด ๋ฐ์ดํฐ๋ ์ฑ์์ ์ ๊ทผ ๋ถ๊ฐ