본문 바로가기
iOS

[iOS] RequestInterceptor를 활용한 토큰 갱신 (1)

by seongminmon 2024. 9. 30.

토큰 갱신 상황

짧은 유효기간을 가진 엑세스 토큰과 상대적으로 긴 유효기간을 가진 리프레시 토큰이 존재

회원가입과 로그인을 제외한 통신들은 헤더에 유효한 엑세스 토큰을 필요로 하고, 엑세스 토큰이 만료되었다면 419번 상태코드를 던져 줌

엑세스 토큰은 만료되고, 리프레시 토큰은 유효한 상황에서, 리프레시 토큰을 사용해 엑세스 토큰을 갱신 시키는 통신이 존재

 

(리프레시 토큰까지 만료가 되면 사용자에게 재로그인을 요청해야 함)

 

따라서 통신시 419번 상태코드를 만나면, 엑세스 토큰 갱신 통신을 하고, 엑세스 토큰 갱신이 성공하면 기존 통신을 재진행해야 함

 

RequestInterceptor란?

Alamofire의 프로토콜로 RequestAdapter, RequestRetrier를 채택하고 있음

adapt와 retry 메서드를 재정의하여 사용 가능함

1. adapt - 요청이 서버로 전송되기 전 실행되는 메서드

2. retry - 요청 실패 시 재시도 여부를 결정하는 메서드

public protocol RequestInterceptor: RequestAdapter, RequestRetrier {}

extension RequestInterceptor {
    public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        completion(.success(urlRequest))
    }

    public func retry(_ request: Request,
                      for session: Session,
                      dueTo error: Error,
                      completion: @escaping (RetryResult) -> Void) {
        completion(.doNotRetry)
    }
}

 

Interceptor가 적용된 통신 Flow (엑세스 토큰 만료 상황)

adapt -> 통신 진행 -> 통신 실패 시 -> retry -> 통신 재시도 시 -> adapt -> 통신 진행

 

전체 코드

final class AuthInterceptor: RequestInterceptor {
    static let shared = AuthInterceptor()
    private init() {}
    
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var urlRequest = urlRequest
        urlRequest.setValue(UserDefaultsManager.accessToken, forHTTPHeaderField: "authorization")
        completion(.success(urlRequest))
    }
    
    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        guard let response = request.task?.response as? HTTPURLResponse,
              response.statusCode == 419 else {
            completion(.doNotRetryWithError(error))
            return
        }
        
        // 토큰 갱신 API 호출
        NetworkManager.shared.refresh { result in
            switch result {
            case .success(_):
                completion(.retry)
            case .failure(let error):
                completion(.doNotRetryWithError(error))
                SceneDelegate.changeWindow(SignInViewController())
            }
        }
    }
}

 

1. adapt

func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
    // 헤더 세팅
    var urlRequest = urlRequest
    urlRequest.setValue(UserDefaultsManager.accessToken, forHTTPHeaderField: "authorization")
    completion(.success(urlRequest))
}

 

adapt에서는 UserDefaults에 저장된 엑세스 토큰을 urlRequest 헤더에 세팅함

구현에 따라 키체인에 저장되어 있을 수 있음

 

adapt -> 통신 진행 -> 통신 실패 시 -> retry -> 통신 재시도 시 -> adapt -> 통신 진행

엑세스 토큰 만료 시 Flow를 살펴보면 adapt가 두 번 호출 됨

첫번째 adapt에서는 헤더를 세팅하는 것이 의미 없지만, retry 이후 갱신된 엑세스 토큰 값을 request에 반영해 주기 위해 헤더를 세팅하는 것

 

2. retry

func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
    guard let response = request.task?.response as? HTTPURLResponse,
          response.statusCode == 419 else {
        completion(.doNotRetryWithError(error))
        return
    }
    
    // 토큰 갱신 API 호출
    NetworkManager.shared.refresh { result in
        switch result {
        case .success(_):
            completion(.retry)
        case .failure(let error):
            completion(.doNotRetryWithError(error))
            SceneDelegate.changeWindow(SignInViewController())
        }
    }
}

 

completion(.doNotRetryWithError(error)) - 기존 통신을 재시도 하지 않음

completion(.retry) - 기존 통신 재시도

 

retry에서는 reponse로 받은 상태코드가 엑세스 토큰 만료를 의미하는 419일 때만 토큰 갱신 api를 호출함

토큰 갱신 api가 성공하면 UserDefaults에 엑세스 토큰을 갱신 후 재시도하고, 실패하였을 때는 리프레시 토큰도 만료되었음을 의미하므로 로그인 화면으로 전환

 

통신에 Interceptor 적용하기

1. moya - provider를 만들 때 Session을 적용해 줌

let provider = MoyaProvider<Router>(session: Session(interceptor: AuthInterceptor.shared))

 

session이 적용되기 위해선 validationType를 .successCodes로 변경해주어야 함

extension TargetType {
    var validationType: ValidationType {
        return .successCodes
    }
}

 

2. Alamofire - session을 만들어 request 진행

let session = Session(interceptor: AuthInterceptor.shared)
session.request(request)

 

더 고려하면 좋을 점

AuthInterceptor를 싱글톤으로 구성해야하는지

화면 전환을 retry 메서드에서 하는 것에 대한 고민

토큰 갱신 api 실패 시 리프레시 토큰 만료가 아닌 다른 이유일 때의 처리

일반 통신 시 엑세스 토큰 만료가 아닌 다른 이유로 실패했을 때도 재요청 고려해보기

 

다음 글

 

[iOS] 재귀 함수를 활용한 토큰 갱신 (2)

이전 글https://k2417000.tistory.com/40 문제 상황프로젝트 진행 중 토큰 갱신을 처리하기 위해 이전에 했던 대로 RequestInterceptor를 사용하려 함기존 retry 함수에서 statusCode를 통해 통신 실패의 이유가 엑

k2417000.tistory.com

'iOS' 카테고리의 다른 글

[iOS] 재귀 함수를 활용한 토큰 갱신 (2)  (0) 2024.10.24
[iOS] 이미지 캐싱 (1) - 메모리 캐싱 (NSCache)  (0) 2024.10.03
[iOS] DateFormatter vs Formatted  (0) 2024.09.26
[iOS] 접근 제어자  (0) 2024.09.07
[iOS] COW(Copy-on-Write)  (0) 2024.08.28