๐ Sign in with Apple
๊ฐ์ฅ ํ๋ผ์ด๋ฒ์ ์นํ์ ์ธ ๋ก๊ทธ์ธ. ์ด๋ฉ์ผ ์จ๊น, 2FA ์๋, Face ID ์ธ์ฆ!
โจ Sign in with Apple์ด๋?
Apple ID๋ก ๊ฐํธ ๋ก๊ทธ์ธํฉ๋๋ค. ์ด๋ฉ์ผ์ ์จ๊ธธ ์ ์๊ณ , 2๋จ๊ณ ์ธ์ฆ์ด ์๋์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค. ๋ค๋ฅธ ์์ ๋ก๊ทธ์ธ์ด ์๋ค๋ฉด ํ์ ๊ตฌํ์ ๋๋ค.
๐ฏ ๊ธฐ๋ณธ ๊ตฌํ
import AuthenticationServices class SignInManager: NSObject, ASAuthorizationControllerDelegate { func signInWithApple() { let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() // ์์ฒญํ ์ ๋ณด (์ด๋ฆ, ์ด๋ฉ์ผ) request.requestedScopes = [.fullName, .email] let controller = ASAuthorizationController(authorizationRequests: [request]) controller.delegate = self controller.presentationContextProvider = self controller.performRequests() } // ์ฑ๊ณต ์ func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization ) { if let credential = authorization.credential as? ASAuthorizationAppleIDCredential { let userID = credential.user // ๊ณ ์ ID (๋ณํ์ง ์์) let email = credential.email // ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต let fullName = credential.fullName // ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต let identityToken = credential.identityToken // JWT ํ ํฐ // ๋ฐฑ์๋๋ก identityToken ์ ์กํ์ฌ ๊ฒ์ฆ if let tokenData = identityToken, let tokenString = String(data: tokenData, encoding: .utf8) { verifyTokenWithBackend(tokenString, userID: userID) } // UserDefaults์ userID ์ ์ฅ UserDefaults.standard.set(userID, forKey: "appleUserID") } } // ์คํจ ์ func authorizationController( controller: ASAuthorizationController, didCompleteWithError error: Error ) { print("๋ก๊ทธ์ธ ์คํจ: \(error.localizedDescription)") } func verifyTokenWithBackend(_ token: String, userID: String) { // ๋ฐฑ์๋ API ํธ์ถ } } extension SignInManager: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return UIApplication.shared.windows.first! } }
๐จ SwiftUI์์ Sign in with Apple ๋ฒํผ
import SwiftUI import AuthenticationServices struct SignInWithAppleButton: View { @Environment(\.colorScheme) var colorScheme var body: some View { SignInWithAppleButton( .signIn, onRequest: { request in request.requestedScopes = [.fullName, .email] }, onCompletion: { result in switch result { case .success(let authorization): handleAuthorization(authorization) case .failure(let error): print("์๋ฌ: \(error)") } } ) .signInWithAppleButtonStyle( colorScheme == .dark ? .white : .black ) .frame(height: 50) } func handleAuthorization(_ authorization: ASAuthorization) { if let credential = authorization.credential as? ASAuthorizationAppleIDCredential { print("์ฌ์ฉ์ ID: \(credential.user)") print("์ด๋ฉ์ผ: \(credential.email ?? \"์์\")") } } } // ์ฌ์ฉ ์์ struct LoginView: View { var body: some View { VStack(spacing: 20) { Text("๋ก๊ทธ์ธ") .font(.largeTitle.bold()) SignInWithAppleButton() .padding(.horizontal) } } }
๐ ์๋ ๋ก๊ทธ์ธ ์ํ ์ฒดํฌ
import AuthenticationServices func checkCredentialState() { guard let userID = UserDefaults.standard.string(forKey: "appleUserID") else { // ์ ์ฅ๋ userID ์์ return } let provider = ASAuthorizationAppleIDProvider() provider.getCredentialState(forUserID: userID) { state, error in switch state { case .authorized: print("๋ก๊ทธ์ธ ์ ์ง ์ค") // ์๋ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ case .revoked: print("๋ก๊ทธ์ธ ์ทจ์๋จ") // ๋ก๊ทธ์์ ์ฒ๋ฆฌ UserDefaults.standard.removeObject(forKey: "appleUserID") case .notFound: print("์ฌ์ฉ์ ์์") case .transferred: print("๋ค๋ฅธ ํ์ผ๋ก ์ด์ ๋จ") @unknown default: break } } } // ์ฑ ์์ ์ ์ฒดํฌ func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { checkCredentialState() return true }
๐ง ์ด๋ฉ์ผ ๋ฆด๋ ์ด (Email Relay)
์ฌ์ฉ์๊ฐ "์ด๋ฉ์ผ ์จ๊ธฐ๊ธฐ"๋ฅผ ์ ํํ๋ฉด Apple์ด ํ๋ก์ ์ด๋ฉ์ผ(`privaterelay@icloud.com`)์ ์ ๊ณตํฉ๋๋ค.
func handleCredential(_ credential: ASAuthorizationAppleIDCredential) { if let email = credential.email { // ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต๋จ print("์ด๋ฉ์ผ: \(email)") // privaterelay@icloud.com ํํ๋ฉด ํ๋ก์ ์ด๋ฉ์ผ if email.contains("privaterelay") { print("์ฌ์ฉ์๊ฐ ์ด๋ฉ์ผ์ ์จ๊น") } // โ ๏ธ ์ค์: ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต๋๋ฏ๋ก ๋ฐ๋์ ์ ์ฅ! UserDefaults.standard.set(email, forKey: "userEmail") } }
๐ ๋ฐฑ์๋ ํ ํฐ ๊ฒ์ฆ
// identityToken์ JWT ํ์์ ๋๋ค // ๋ฐฑ์๋์์ Apple ๊ณต๊ฐํค๋ก ๊ฒ์ฆํด์ผ ํฉ๋๋ค func sendTokenToBackend(_ credential: ASAuthorizationAppleIDCredential) async { guard let tokenData = credential.identityToken, let token = String(data: tokenData, encoding: .utf8) else { return } // authorizationCode๋ ๋ฐฑ์๋๋ก ์ ์ก (refresh token ๋ฐ๊ธ์ฉ) guard let codeData = credential.authorizationCode, let code = String(data: codeData, encoding: .utf8) else { return } // API ํธ์ถ let body: [String: Any] = [ "identityToken": token, "authorizationCode": code, "user": credential.user ] // POST to your backend } /* ๋ฐฑ์๋ ๊ฒ์ฆ (Node.js ์์) const jwt = require('jsonwebtoken'); const jwksClient = require('jwks-rsa'); const client = jwksClient({ jwksUri: 'https://appleid.apple.com/auth/keys' }); function getKey(header, callback) { client.getSigningKey(header.kid, (err, key) => { callback(null, key.publicKey || key.rsaPublicKey); }); } jwt.verify(identityToken, getKey, (err, decoded) => { if (err) { // ํ ํฐ ๊ฒ์ฆ ์คํจ } else { // decoded.sub === userID // decoded.email === ์ฌ์ฉ์ ์ด๋ฉ์ผ } }); */
๐ฑ ๊ธฐ์กด ๊ณ์ ์ ๊ทธ๋ ์ด๋
import AuthenticationServices // ๊ธฐ์กด ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ๋ก๊ทธ์ธ์ Apple ID๋ก ์ ๊ทธ๋ ์ด๋ func upgradeToAppleID(existingEmail: String) { let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName] // ๊ธฐ์กด ๊ณ์ ์ฐ๊ฒฐ์ ์ํ ํํธ request.user = existingEmail let controller = ASAuthorizationController(authorizationRequests: [request]) // ... delegate ์ค์ controller.performRequests() }
๐จ ๋ก๊ทธ์์ & ๊ณ์ ์ญ์
// ๋ก๊ทธ์์ func signOut() { UserDefaults.standard.removeObject(forKey: "appleUserID") UserDefaults.standard.removeObject(forKey: "userEmail") // UI๋ฅผ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก } // ๊ณ์ ์ญ์ (๋ฐฑ์๋์์ Apple API ํธ์ถ ํ์) /* ์ฌ์ฉ์๊ฐ "์ค์ > Apple ID > ์ํธ ๋ฐ ๋ณด์ > Apple๋ก ๋ก๊ทธ์ธ์ ์ฌ์ฉํ๋ ์ฑ"์์ ์ฑ์ ์ ๊ฑฐํ๋ฉด ๋ก๊ทธ์ธ์ด ์ทจ์๋ฉ๋๋ค. ๋ฐฑ์๋์์๋ Apple์ Token Revocation API๋ฅผ ํธ์ถํด์ผ ํฉ๋๋ค: POST https://appleid.apple.com/auth/revoke ์ดํ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๊ณ ๊ณ์ ์ ๋นํ์ฑํํฉ๋๋ค. */
โ๏ธ ์ฑ ์ค์ (Capabilities)
1. Xcode์์ ํ๋ก์ ํธ ์ ํ
2. Signing & Capabilities ํญ
3. "+ Capability" ํด๋ฆญ
4. "Sign in with Apple" ์ถ๊ฐ
5. Apple Developer ์ฝ์์์:
- Identifier์ "Sign in with Apple" ํ์ฑํ
- Services ID ์์ฑ (์น ๋ก๊ทธ์ธ์ฉ, ์ ํ)
- Key ์์ฑ (๋ฐฑ์๋ ๊ฒ์ฆ์ฉ)โ ํ์ ์๊ตฌ์ฌํญ (App Review)
โ ๏ธ ํ์!
์ฑ์ ๋ค๋ฅธ ์์
๋ก๊ทธ์ธ(Google, Facebook ๋ฑ)์ด ์๋ค๋ฉด,
Sign in with Apple์ ๋ฐ๋์ ๊ตฌํํด์ผ ํฉ๋๋ค.
๊ทธ๋ ์ง ์์ผ๋ฉด App Store ์ฌ์ฌ์์ ๋ฆฌ์ ๋ฉ๋๋ค.
๐ก HIG ์ฒดํฌ๋ฆฌ์คํธ
โ
๊ณต์ Sign in with Apple ๋ฒํผ ์ฌ์ฉ
โ
๋ค๋ฅธ ๋ก๊ทธ์ธ ๋ฒํผ๊ณผ ๋์ผํ ํฌ๊ธฐ/์์น
โ
์ด๋ฉ์ผ/์ด๋ฆ์ ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต๋จ (์ ์ฅ ํ์)
โ
ํ ํฐ์ ๋ฐฑ์๋์์ ๊ฒ์ฆ
โ
์ฑ ์์ ์ credential ์ํ ์ฒดํฌ