RequestInterceptor는 Alamofire에서 제공하는 프로토콜입니다.
Moya는 Alamofire를 wrapping한 라이브러리이기 때문에 해당 프로토콜을 사용할 수 있습니다.
RequestInterceptor에서 RequestAdapter과 RequestRetrier를 채택하고 있습니다. 이름 그대로 통신을 Interceptor하여 작업을 수행합니다.
또한 해당 프로토콜은 각각 adapt, retry 함수를 요구합니다.
그리고 RequestInterceptor는 extension을 통해 이 adapt와 retry의 기본 구현을 제공합니다.
adapt는 Request가 전송되기 전에 원하는 추가적인 작업을 수행할 수 있도록 하는 함수입니다. 즉, 이 함수에 다음과 같이 헤더에 넣을 토큰을 지정해주면 별도로 TargetType마다 헤더에 토큰을 넣어주지 않아도 자동으로 토큰이 들어가게 됩니다.
AuthInterceptor의 adapt와 retry 메서드
adapt 메서드
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
if let accessToken = authService.getAccessToken() {
urlRequest.addValue("Bearer \\(accessToken)", forHTTPHeaderField: "Authorization")
}
completion(.success(urlRequest))
}
이 adapt 메서드는 모든 네트워크 요청이 서버로 전송되기 전에 호출됩니다. 전체적인 로직으로는
- authService에서 저장된 액세스 토큰을 가져옵니다.
- 액세스 토큰이 있다면, 요청의 헤더에 "Authorization" 필드를 추가하고 "Bearer" 스키마와 함께 토큰을 설정합니다.
- 수정된 요청을 completion 핸들러를 통해 반환합니다.
이렇게 하면 모든 API 요청에 자동으로 인증 정보가 포함되어 전송됩니다.
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 == 401 else {
completion(.doNotRetry)
return
}
guard let refreshToken = authService.getRefreshToken() else {
authService.clearTokens()
completion(.doNotRetry)
return
}
provider.request(.refreshToken(refreshToken: refreshToken)) { [weak self] result in
switch result {
case .success(let response):
do {
let reissueResponse = try response.map(ResponseBodyDTO<ReissueModel>.self)
if reissueResponse.success, let data = reissueResponse.data {
self?.authService.saveAccessToken(data.accessToken)
self?.authService.saveRefreshToken(data.refreshToken)
completion(.retry)
} else {
self?.authService.clearTokens()
completion(.doNotRetry)
}
} catch {
self?.authService.clearTokens()
completion(.doNotRetry)
}
case .failure:
self?.authService.clearTokens()
completion(.doNotRetry)
}
}
}
retry 메서드는 네트워크 요청이 실패했을 때 호출됩니다.
- 응답 상태 코드가 401(Unauthorized)인지 확인합니다. 401이 아니면 재시도하지 않습니다.
- 리프레시 토큰을 가져옵니다. 없으면 저장된 토큰을 모두 삭제하고 재시도하지 않습니다.
- 리프레시 토큰을 사용해 새로운 액세스 토큰을 요청합니다.
- 새 토큰 발급에 성공하면 토큰을 저장하고 원래 요청을 재시도합니다.
- 실패하면 저장된 토큰을 모두 삭제하고 재시도하지 않습니다.
AuthInterceptor를 사용하기 위해서는 Moya Provider를 생성할 때 이를 설정해야 합니다.
swift
Copy
let provider = MoyaProvider<MyAPI>(plugins: [NetworkLoggerPlugin()], interceptor: AuthInterceptor.shared)
이렇게 설정하면 이 provider를 통해 이루어지는 모든 네트워크 요청에 AuthInterceptor가 적용됩니다.
자동로그인을 구현해보자!
- 자동 로그인과 액세스 토큰 사용
자동 로그인 구현:
func autoLogin(completion: @escaping (Bool) -> Void) {
guard let refreshToken = authService.getRefreshToken() else {
loginState.value = .notLogin
completion(false)
return
}
provider.request(.refreshToken(refreshToken: refreshToken)) { [weak self] result in
switch result {
case .success(let response):
do {
let reissueResponse = try response.map(ResponseBodyDTO<RefreshTokenResponseModel>.self)
if reissueResponse.success, let data = reissueResponse.data {
self?.saveTokens(accessToken: data.accessToken, refreshToken: data.refreshToken)
self?.fetchUserInfo(completion: completion)
} else {
self?.clearTokensAndHandleError()
completion(false)
}
} catch {
self?.clearTokensAndHandleError()
completion(false)
}
case .failure:
self?.clearTokensAndHandleError()
completion(false)
}
}
}
이 autoLogin 함수는 앱 시작 시 호출되어 자동 로그인을 시도합니다. 전체적인 과정으로는
- 저장된 리프레시 토큰을 확인합니다.
- 리프레시 토큰을 사용해 새로운 액세스 토큰을 요청합니다.
- 성공하면 새 토큰을 저장하고 사용자 정보를 가져옵니다.
- 실패하면 토큰을 삭제하고 로그인 상태를 업데이트합니다.
또한 AuthInterceptor를 통해 모든 요청에 자동으로 액세스 토큰이 포함되므로, 다른 서비스에서는 별도로 토큰을 처리할 필요가 없습니다. 예를 들어:
func updateUserProfile() {
provider.request(.userProfile) { result in
switch result {
case .success(let response):
//성공시
case .failure(let error):
}
}
}
이 fetchUserProfile 함수에서는 별도의 토큰 처리 로직이 없지만, AuthInterceptor에 의해 자동으로 액세스 토큰이 요청 헤더에 포함됩니다.