UIKit - Refresh Token 만료 시 로그인 화면 전환 처리하기
사용자가 로그인하여 발급받은 Access Token이 만료되었을 경우 같이 발급된 Refresh Token을 통해 재발급 처리를 위해 공통 요청 객체를 아래와 같이 구성해뒀음.
NetworkingWrapper.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import UIKit
final class NetworkingWrapper {
public static let shared = NetworkingWrapper()
private let authService: AuthService = AuthService.shared
private let session: URLSession = URLSession(configuration: .default)
private var isLoading: Bool = false
private init() {}
public func request(
urlRequest: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) {
var request = urlRequest
// Access, Refresh Token read
guard let accessToken = KeyChainUtil.read(key: Constants.ACCESS_TOKEN),
let refreshToken = KeyChainUtil.read(key: Constants.REFRESH_TOKEN)
else { return }
// Bearer Header 설정
request.setValue(
"Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
// HTTP 요청
session.dataTask(with: request) { data, response, error in
// HTTP Status가 403인 경우
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 403
{
if self.isLoading { return }
self.isLoading = true
// 토큰 재발급 API 호출
self.authService.tokenRefresh(refreshRequest: RefreshTokenRequest(refreshToken: refreshToken)) { result in
switch result {
case .success(let response):
// 성공 시 토큰을 업데이트한 뒤 원래 요청을 다시 수행
guard let response = response else {
self.isLoading = false
return
}
KeyChainUtil.create(key: Constants.ACCESS_TOKEN, data: response.accessToken)
KeyChainUtil.create(key: Constants.REFRESH_TOKEN, data: response.refreshToken)
self.request(urlRequest: request, completionHandler: completionHandler)
case .failure(let error):
print("error :\(error)")
}
self.isLoading = false
}
return
}
completionHandler(data, response, error)
}.resume()
}
}
Refresh Token이 만료되었을 경우에는 로그인을 다시 해줘야하기 때문에 로그인 화면으로 전환할 수 있도록 처리해줘야했음.
토큰 재발급을 실패할 경우 해당 API를 호출하는 화면이라면 전부 에러를 반환받을 수 있기 때문에 전역적으로 관리가 필요했음.
Delegate나 Closure처럼 특정 객체에만 이벤트를 보내는 것보다는 NotificationCenter를 통해 Notification을 보내 처리하는 것이 더 적절하다고 생각했음.
또한, rootViewController에 접근이 가능한 SceneDelegate 내에서 로직을 구현하기로 결정함.
NotificationCenterName+TokenDeleted
1
2
3
4
5
import Foundation
extension Notification.Name {
static let tokenDeleted = Notification.Name("tokenDeleted")
}
NetworkingWrapper.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import UIKit
final class NetworkingWrapper {
public static let shared = NetworkingWrapper()
private let authService: AuthService = AuthService.shared
private let session: URLSession = URLSession(configuration: .default)
private var isLoading: Bool = false
private init() {}
public func request(
urlRequest: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) {
var request = urlRequest
// Access, Refresh Token read
guard let accessToken = KeyChainUtil.read(key: Constants.ACCESS_TOKEN),
let refreshToken = KeyChainUtil.read(key: Constants.REFRESH_TOKEN)
else { return }
// Bearer Header 설정
request.setValue(
"Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
// HTTP 요청
session.dataTask(with: request) { data, response, error in
// HTTP Status가 403인 경우
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 403
{
if self.isLoading { return }
self.isLoading = true
// 토큰 재발급 API 호출
self.authService.tokenRefresh(refreshRequest: RefreshTokenRequest(refreshToken: refreshToken)) { result in
switch result {
case .success(let response):
// 성공 시 토큰을 업데이트한 뒤 원래 요청을 다시 수행
guard let response = response else {
self.isLoading = false
return
}
KeyChainUtil.create(key: Constants.ACCESS_TOKEN, data: response.accessToken)
KeyChainUtil.create(key: Constants.REFRESH_TOKEN, data: response.refreshToken)
self.request(urlRequest: request, completionHandler: completionHandler)
case .failure(let error):
// 에러 발생 시 토큰 제거 및 로그인 화면 전환
KeyChainUtil.delete(key: Constants.ACCESS_TOKEN)
KeyChainUtil.delete(key: Constants.REFRESH_TOKEN)
NotificationCenter.default.post(name: .tokenDeleted, object: nil)
}
self.isLoading = false
}
return
}
completionHandler(data, response, error)
}.resume()
}
}
SceneDelegate.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// NotificationCenter Observer 등록
NotificationCenter.default.addObserver(self, selector: #selector(tokenDeleted), name: .tokenDeleted, object: nil)
}
func changeRootViewController(viewController: UIViewController){
guard let window = window else { return }
UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve) {
window.rootViewController = viewController
window.makeKeyAndVisible()
}
}
@objc private func tokenDeleted() {
DispatchQueue.main.async {
self.changeRootViewController(viewController: UINavigationController(rootViewController: LoginViewController()))
}
}
}
This post is licensed under CC BY 4.0 by the author.