- 우리 서버 백엔드 설명
- 애플 공식 문서
Apple OAuth 작동 시퀀스
- 클라이언트가 애플 서버로 사용자에 대한 credential 요청
- 수신한 credential에서 JWT token 형식의 identity token 추출
- 추출한 identity token을 우리 서버로 송신
- 우리 서버에서 해당 identity token으로 애플 서버에 사용자 식별 요청
- 필요 시, authorization code와 identity token을 바탕으로 우리 서버가 애플 서버에 access token과 refresh token 발급 요청 가능
- 우리 서버에서 클라이언트로 사용자에 대한 access token과 refresh token을 송신
이 글에서 소개할 방식은 5번 과정을 생략한다. 대신 사용자 식별 후 우리 서버에서 사용자에 대한 자체 access token과 refresh token을 포함한 JWT 토큰을 발행해 클라이언트로 넘겨주는 방법에 대해서 소개한다.
이 글에서는 iOS에 대해서만 다루며, 백엔드에 대한 글은 상단 링크를 참조
Apple 서버에 credentials 및 user info 요청
먼저 로그인 기능 사용을 위한 라이브러리를 import 한 뒤 Model과 ViewModel을 만들어주고 ASAuthorizationControllerDelegate 프로토콜과 ASAuthorizationControllerPresentationContextProviding을 구현한다.
import AuthenticationServices
Model
//Model
struct AppleLoginInfo: Codable {
var token: String?
}
ViewModel
loginApple()를 호출하는 것으로 애플 로그인 시퀀스가 시작된다. 애플 로그인 시 보안을 위한 권고 사항 중 하나인 nonce값도 request에 포함시켰다. 함수 authorizationController()에서 credential의 identity token을 추출해 함수 insertAppleIdTokenToAppleLoginInfoModel()의 인자로 넘겨주고 있다.
class AppleLoginViewModel: NSObject, ObservableObject {
var appleLoginInfo = AppleLoginInfo()
func loginApple() {
// 애플 로그인 요청 시 사용되는 요청 객체 생성
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = encdoeNonceSha256(nonce)
// 인증 요청 컨트롤러 생성
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
}
// ASAuthorizationControllerDelegate 프로토콜 구현
extension MemberViewModel: ASAuthorizationControllerDelegate {
// 인증이 성공적으로 완료되었을 때 호출되는 콜백 함수
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
insertAppleIdTokenToAppleLoginInfoModel(appleIdToken: String(decoding: appleIDCredential.identityToken!, as: UTF8.self))
print("\(appleIDCredential.user)의 인증서 발급 성공")
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
self.error = error
print("인증(로그인) 실패 error : \(error)")
}
}
// ASAuthorizationControllerPresentationContextProviding 구현
extension MemberViewModel: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
// 로그인 화면이 표시될 컨텍스트를 제공
guard let window = UIApplication.shared.windows.first else {
fatalError("No window found.")
}
return window
}
}
추출한 identity token으로 우리 서버에 애플 로그인 요청
appleLoginInfo 프로퍼티에 추출한 identity token을 담고 이를 우리 서버에 애플 로그인을 요청하는 함수 requestAppleLoginToServer()의 인자로 넘겨준다.
func insertAppleIdTokenToAppleLoginModel(appleIdToken: String) {
appleLoginInfo.token = appleIdToken
if appleLoginInfo.token != nil {
// print("애플 토큰 : ", appleLoginInfo.token)
requestAppleLoginToServer(appleLoginInfo: appleLoginInfo)
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("애플 로그인 비동기 error: \(error)")
case .finished:
print("애플 로그인 비동기 종료")
break
}
}, receiveValue: { member in
})
.store(in: &self.loginCancellables)
}
}
함수 reqeustAppleLoginToServer() 는 비동기로 작업을 수행하고, 만약 우리 서버에 보내는 identity 토큰 형식이 유효하지 않을 경우에는 이를 처리하는 로직을 추가하면 된다.
func requestAppleLoginToServer(appleLoginInfo: AppleLoginInfo) -> Future<{반환할 타입}, Error> {
return Future { promise in
guard let url = URL(string: "{엔드포인트}") else {
fatalError("Invalid URL")
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
do {
let jsonData = try JSONEncoder().encode(appleLoginInfo)
request.httpBody = jsonData
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
} catch {
promise(.failure(error))
}
URLSession.shared.dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.global(qos: .background))
.receive(on: DispatchQueue.main)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
if httpResponse.statusCode != 200 {
throw URLError(.badServerResponse)
print("유효하지 않은 apple identity token")
}
return data
}
.decode(type: {디코드할 타입}.self, decoder: JSONDecoder())
.sink { completion in
print("Completion: \(completion)")
promise(.success({성공시 반환할 인스턴스}))
} receiveValue: { data in
//우리 서버로부터 수신한 data 처리
}
.store(in: &self.loginCancellables)
}
}
로그인 View 구현
이제 함수 loginApple()을 호출할 로그인 뷰를 구현해보자. 애플은 애플 로그인 아이콘에 대한 가이드와 기본 인터페이스를 제공하며 이는 공식 문서에서 확인 가능하다. 나는 직접 만듦.
View
import SwiftUI
struct LoginView: View {
@ObservedObject var appleLoginViewModel: AppleLoginViewModel = AppleLoginViewModel
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.ivory)
VStack {
Button(action: {
memberVM.loginApple()
}, label: {
appleDefaultIcon()
})
}
}
.ignoresSafeArea()
}
@ViewBuilder
func appleDefaultIcon() -> some View {
RoundedRectangle(cornerRadius: 6.3)
.frame(width: screenWidth * 0.8, height: 48)
.foregroundColor(.black)
.overlay(
HStack {
Image(systemName: "apple.logo")
.foregroundColor(.white)
.font(.system(size: 19))
.offset(y:-1.5)
Text("Continue with Apple")
.foregroundColor(.white)
.font(.system(size: 17))
}
)
}
}
'iOS' 카테고리의 다른 글
[iOS] [SwiftUI] [TCA] TCA에 대해 알아보고 간단한 튜토리얼을 완료해보자 (0) | 2023.12.16 |
---|---|
[iOS] [Swift] 리프레시 토큰 도입기 (0) | 2023.12.16 |