본문 바로가기

iOS

[swift/iOS] 애플 로그인 & 로그아웃 구현까지의 과정

이번에는 어떻게 애플 로그인 & 로그아웃을 프로젝트에 적용했는지 정리해보려고한다!

서버분도 나도 처음 애플 로그인을 구현하시는거라 서로 많이 애를 먹었다....

 

먼저, 애플 로그인을 구현하기 위해서는 애플 개발자 계정이 필요한데

해당과정은 이미 다른 분들이 포스트를 많이 해놓으셨기 때문에 나는 코드적인 부분만 다뤄 볼 예정이다.

 

1. 로그인 버튼을 눌렀을 때

2. 로그인이 성공적으로 되었을 때 or 실패했을 때

3. 앱이 실행되었을 때

3-1 앱이 백그라운드에서 다시 넘어왔을 때

4. 자동 로그인

5. 로그아웃

 

 

1. 로그인 버튼을 눌렀을 때

먼저 애플 로그인을 구현하기 위해서는 AuthenticationServices 라이브러리를 사용해줘야한다.

    @objc private func didTapAppleLoginButton() {
        let request: ASAuthorizationAppleIDReqeust = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]
        
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self as? ASAuthorizationControllerPresentationContextProviding
        controller.performRequests()
    }

먼저 ASAuthorizationAppleIDProvider() 클래스의 createReqeust() 함수를 통해

사용자에게 AppleID에 대한 접근을 요청하는 ASAuthorizationAppleIDReqeust객체를 만들어야 한다.

 

만든 객체의 requestedScopes를 통해 사용자에게 받는 정보를 설정할 수 있다.

두 가지의 Scopes를 설정할 수 있는데 사용자의 이름과, 이메일을 받을 수 있다.

 

그 다음 ASAuthorizationController 클래스를 위에서 만든 ASAuthorizationAppleIDReqeust를 이용해 만든다.

ASAuthorizationController 클래스는 애플 로그인 인증 요청을 관리하는 컨트롤러이다.

 

2. 로그인이 성공적으로 되었을 때 or 실패했을 때

performRequest() 함수를 통해 클라이언트 상에서 애플 로그인이 성공하면 

인증 인스턴스와 함께 아래의 메소드를 호출합니다.

func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization)

만약 로그인이 실패하게 되면 아래의 메소드를 호출한다.

    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error)

 

위의 두 메소드는 ASAuthorizationControllerDelegate 프로토콜로 구현한다.

extension LoginViewController: ASAuthorizationControllerDelegate {

    // 성공 후 동작
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization)
    {
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
              let code = credential.authorizationCode else {
            return
        }
        
        didSuccessdAppleLogin(code, credential.user) // 아래에 코드 있음
    }
    
    // 실패 후 동작
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error)
    {
        print("애플 로그인 실패")
    }
}

위에서 성공을 통해서 authorization에는 인증 인스턴스가 넘어오게 되고 authorization.crednetial로 사용자의 정보를 받아올 수 있다.

혹시나 해당 값을 안전하게 가져오기 위해 guard let 을 이용해 주었다.

 

로그인에 성공하면 crendential에는 identityToken, authorizationCode, user, email 이렇게 총 4개의 값을 리턴 받는데

서버쪽에서 authorizationCode와 user정보만 전달해주면 처리해주기 때문에 해당 값을 서버로 보냈다.

 

    private func didSuccessdAppleLogin(_ code: Data, _ user: String) {
        let autorizationCodeStr = String(data: code, encoding: .utf8)
        let parameter = ["accessToken": autorizationCodeStr!]
        
        API.appleOAuthLogin(parameter) { [weak self] info in
            DispatchQueue.main.async {
                print("appleOAutoLogin 응답: \(info)")
                LoginManager.shared.saveAppleLoginInfo(info)
                keyChain.create(userID: user)
                
                self?.goToInitialViewController()
            }
        }
    }

 

해당 메소드는 서버로 유저 정보와 코드를 전달하는 메소드이고 로그인에 성공하면 keyChain을 통해 user값을 저장했다.

이 user정보는 나중에 사용자가 앱을 실행했을 때나 앱이 백그라운드 상태에서 넘어왔을 때 로그인의 상태를 파악하는데 사용된다.

또한 서버로 유저 정보와 코드를 전달하면 rememberMeToken을 리턴해주는데 이 값이 나중에 자동 로그인과 로그아웃을 구현하는데 필요하다. 해당 과정은 아래서 설명하겠다.

 

keyChain 클래스는 아래의 코드로 구현했다.

class keyChain {
    
    class func create(userID: String) {
        let query: NSDictionary = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: "userID",
            kSecValueData: userID.data(using: .utf8, allowLossyConversion: false) as Any
        ]
        
        SecItemDelete(query)
        
        let status = SecItemAdd(query, nil)
        assert(status == noErr, "아이디 저장 실패")
    }
    
    class func read() -> String? {
        let query: NSDictionary = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: "userID",
            kSecReturnData: kCFBooleanTrue as Any,
            kSecMatchLimit: kSecMatchLimitOne
        ]
        
        var dataTypeRef: AnyObject?
        let status = SecItemCopyMatching(query, &dataTypeRef)

        if status == errSecSuccess {
            let retrievedData = dataTypeRef as! Data
            let value = String(data: retrievedData, encoding: String.Encoding.utf8)
            return value
        } else {
            return ""
        }
    }
    
    class func delete() {
        let query: NSDictionary = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: "userID"
        ]
        
        let status = SecItemDelete(query)
        assert(status == noErr, "아이디 삭제 실패")
    }
}

 

3. 앱이 실행되었을 때 - AppDelegate

    func appleLogin() {
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        appleIDProvider.getCredentialState(forUserID: keyChain.read()!) { credentialState, error in
            switch credentialState {
            case .authorized:
                // 인증 성공 상태
                UserDefaults.standard.removeObject(forKey: "GoogleLoginInfo")
                print("애플 로그인 인증 성공")
                break
            case .revoked:
                // 인증 만료 상태 (사용자가 백그라운드에서 ID중지했을 경우)
                print("애플 로그인 인증 만료")
                // 만약 애플 로그인이 로그인 상태였으면 로그아웃 상태로 해야 함
                
                if keyChain.read() != "" {
                    LoginManager.shared.logout { }
                }
        
                break
            case .notFound:
                // Credential을 찾을 수 없는 상태 (로그아웃 상태)
                print("애플 Credential을 찾을 수 없음")
                break
            default:
                break
            }
        }
    }

keyChain.read()!에서 만약에 사용자가 애플 로그인을 하지 않았을 경우를 대비해서 만약 로그인 하지 않은 경우에는 ""와 같이 빈값이 리턴되도록 했다.

해당 메소드는 crenditalState에 따라서 분기로 처리해주면 되는데

.authorized -> 인증 성공 상태

.revoked -> 인증 만료 상태

.notFound -> Credential을 찾을 수 없는 상태

 

각각의 상태마다 해당하는 로직을 구현해주면 된다.

 

.authorized일때는 애플 로그인와 구글 로그인을 지원하고 있기 때문에 로그인에 혼동이 나서는 안되기 때문에

애플 로그인이 성공적으로 인증에 성공하면 구글 로그인 정보는 지워주고있다. 

.revoked일때는 애플 로그인이 로그인 상태이면 로그아웃 상태로 바꿔주고 있다.

 

3-1 앱이 백그라운드 상태에서 넘어왔을 때

   func sceneDidBecomeActive(_ scene: UIScene) {
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        appleIDProvider.getCredentialState(forUserID: keyChain.read()!) { credentialState, error in
            switch credentialState {
            case .authorized:
                // 인증 성공 상태
                print("sceneDidBecomeActive - 애플 로그인 인증 성공")
                break
            case .revoked:
                // 인증 만료 상태 (사용자가 백그라운드에서 ID중지했을 경우)
                print("sceneDidBecomeActive - 애플 로그인 인증 만료")
                // 만약 애플 로그인이 로그인 상태였으면 로그아웃 상태로 해야 함
                
                if keyChain.read() != "" {
                    LoginManager.shared.logout {
                        DispatchQueue.main.async {
                            let homeViewController = UINavigationController(rootViewController: HomeViewController())
                            
                            self.window?.rootViewController = homeViewController
                            self.window?.rootViewController?.dismiss(animated: false)
                        }
                    } 
                }
                
                break
            case .notFound:
                // Credential을 찾을 수 없는 상태 (로그아웃 상태)
                print("sceneDidBecomeActive - 애플 Credential을 찾을 수 없음")
                break
            default:
                break
            }
        }
    }

앱이 백그라운드에서 넘어왔을 때도 위와 마찬가지로 분기처리를 해주면 된다.

해당 부분을 구현해주지 않으면 백그라운드 상황에서 Apple ID 사용 중지를 했을 때 로그인 상태가 꼬이게 된다.

따라서 백그라운드에서 Apple ID 사용 중지를 하면 로그아웃을 해야된다.

이것 또한 필수적으로 구현해줘야 한다.

 

4. 애플 로그인 로직

위의 과정을 통해 authorizationCode와 user정보를 서버로 보내면 서버쪽에서 rememberMeToken값을 리턴해준다.

사실상 이 Token값으로 로그인을 구현하는 것이라고 봐도 무방하다.

 

위의 과정들은 서버가 애플 로그인을 구현하기 위해 필요한 정보를 보낸것이라고 생각하면 된다.

즉, 내가 보냈던 authorizationCode와 user정보는 서버쪽에서 애플에게  인증을 받기 위해 보낸것이고

인증을 마친 서버가 rememberMeToken을 클라이언트에게 주면 클라이언트는 이 값으로 다시 서버로 로그인 인증을 하는 방식이라고 보면된다. 실제로 위에 코드를 보면 애플 로그인이 성공되고 난 뒤에 keyChain으로 user정보를 저장하는것 밖에는 하지 않는다.

 

그래서, 사용자가 애플 로그인 버튼을 누르고 서버와의 통신을 통해서 rememberMeToken을 받으면

rememberMeToken을 UserDefaults로 저장해주어야 한다. -> 해당 값으로 로그인 이용

// LoginViewController

API.appleOAuthLogin(parameter) { [weak self] info in
            DispatchQueue.main.async {
                print("appleOAutoLogin 응답: \(info)")
                LoginManager.shared.saveAppleLoginInfo(info)
                keyChain.create(userID: user)
                
                self?.goToInitialViewController()
            }
        }
// LoginManager

func saveAppleLoginInfo(_ info: LoginInfo) {
        appleLoginInfo = info
        UserDefaults.standard.set(try? PropertyListEncoder().encode(info), forKey: "AppleLoginInfo")
        
        print("애플 로그인 정보 저장 완료")
    }

 

즉, 로그인을 성공하고 메인 뷰에 들어왔을 때

해당 UserDeafults로 저장한 값 rememberMeToken을 통해 로그인을 하는 것이다.

이 값은 자동 로그인에도 쓰인다. 해당 값이 있으면 로그인한 기록이 있으면서 로그인 상태이고, 값이 없으면 로그인한 기록이 없고 로그아웃 상태인 것이다.

그래서 rememberMeToken을 통해서 서버쪽으로 또 로그인 요청을 보내고 성공하면 로그인 상태로 true로 만든다.

이 과정을 통해 사용자가 앱을 껏다 켜도 다시 애플 로그인을 입력받지 않고도 자동으로 로그인이 되도록 하는 것이다. 

그런데 만약 UserDefults에 저장한 값이 없으면 로그인 상태를 false로 만들고 사용자에게 로그인을 입력받아야된다.

 

되게 복잡하게 설명을 했는데.. 코드를 보면 매우 간단한 로직이다.

 

// LoginManager
func checkLogin(completion: @escaping() -> Void) {

        loadLoginInfo()
        
        // 애플 로그인 한 경우
        if appleLogin == true {
            
            let parameter: [String: Any] = [
                "id": appleLoginInfo!.userID,
                "token": appleLoginInfo!.rememberMeToken
            ]
            
            API.rememberedLogin(parameter) { info in
                guard let info = info else {
                    self.isLoggedIn = false
                    completion()
                    return
                }
                
                self.saveAppleLoginInfo(info)
                self.isLoggedIn = true
                self.firstLogin = true
                completion()
            }
        } else {
            self.isLoggedIn = false
            completion()
        }
    }
// LoginManager

func loadLoginInfo() {
	if keyChain.read() != "" {
            if let data = UserDefaults.standard.data(forKey: "AppleLoginInfo") {
                
                appleLoginInfo = try? PropertyListDecoder().decode(
                    LoginInfo.self,
                    from: data)
                
                appleLogin = true
                print("자동 로그인 정보 있음 - 애플")
            }
        } else {
            print("자동 로그인 정보 없음")
        }
    }

 

메인 뷰가 켜지게 되면 

vidDidLoad()에서 checkLogin()이 실행되면서

로그인 정보가 있는지 없는지 loadLoginInfo()을 실행하고 해당 값에서 로그인 정보가 있으면

그 rememberMeToken을 통해 로그인을 진행하고 로그인 상태를 true로 만들고 없으면 로그인 상태를 false로 만든다.

 

즉, 로그인 상태가 false이면 서버로 애플 로그인 정보를 보내게되고 rememberMeToken을 받아서 메인뷰로와 해당 값을 통해 로그인을을 진행하는 것이다.

 

5. 로그아웃

그 다음은 로그아웃을 구현해야한다..

 

func logout(completion: @escaping () -> Void) {

	guard let cookies = HTTPCookieStorage.shared.cookies else { return }
            
	for cookie in cookies {
	HTTPCookieStorage.shared.deleteCookie(cookie)
	}
    
	keyChain.delete()
	self.appleLoginInfo = nil
	UserDefaults.standard.removeObject(forKey: "AppleLoginInfo")
	self.appleLogin = false
       completion()
        }
    }

먼저, keyChain에 저장되어있던 사용자의 애플 로그인 정보를 삭제해준다.

그리고 UserDefaults에 저장되어있던 rememberMeToken도 삭제해주고 로그인 상태를 false로 만들어준다.

 

이렇게 되어도 사용자의 앱에는 한 번 로그인하면 Apple ID가 저장되있어서 만약

다시 로그인을 한다고 해도 사용자가 앱상에서 번거롭게 처음 로그인했을 때와 같은 과정은 거치지 앟는다.

 

6. 회원탈퇴

마지막으로 회원탈퇴다. 

이거는 서버쪽에서 로그인 한 상태에서 회원탈퇴 버튼을 누르게 되면 다 처리되게 해주셨기 때문에 간단했다.

 

이상 끝!