요청 결과가 401(Unauthorized)
일 때는 자동으로 토큰을 재발급 받는 게 필요하다. 찾기 힘들었지만, https://github.com/Moya/Moya/issues/1177#issuecomment-345132374 에서 도움을 받았다.
request 할 때 401이면 자동으로 토큰 재발급을 요청하고, 제대로 된 토큰이 넘어오면 UserDefaults에 저장하고, 다시 원래 api를 요청할 때 헤더에 UserDefaults에 저장한 토큰을 사용하면 된다.
전체 코드는 https://gist.github.com/susemi99/841b2c3935b2028b2162842d479de143 에 올려뒀다.
provider.rx.request(api) .flatMap { // 401(Unauthorized) 발생 시 자동으로 토큰을 재발급 받는다 if $0.statusCode == 401 { throw TokenError.tokenExpired } else { return Single.just($0) } } .retryWhen { (error: Observable<TokenError>) in error.flatMap { error -> Single<Response> in AuthService.shared.renewalToken() // 토큰 재발급 받기 } } .handleResponse() .filterSuccessfulStatusCodes() .retry(2)
import Foundation import Moya import RxSwift /// 서버에서 보내주는 오류 문구 파싱용. extension PrimitiveSequence where Trait == SingleTrait, Element == Response { func handleResponse() -> Single<Element> { return flatMap { response in // 토큰 재발급 받았을 때 토큰 변경함 if let newToken = try? response.map(Token.self) { UserDefaults.accessToken = newToken.accessToken UserDefaults.refreshToken = newToken.refreshToken } if (200 ... 299) ~= response.statusCode { return Single.just(response) } if var error = try? response.map(ResponseError.self) { error.statusCode = response.statusCode return Single.error(error) } // Its an error and can't decode error details from server, push generic message let genericError = ResponseError(statusCode: response.statusCode serverName: "unknown Server Name", error: "unknown error", message: "empty message") return Single.error(genericError) } } } /// 토큰 만료 에러 enum TokenError: Swift.Error { case tokenExpired }
/// 인증 관련 API final class AuthService: BaseService<AuthAPI> { static let shared = AuthService() private override init() {} /// 토큰 재발급 func renewalToken(refreshToken: String) -> Single<Response> { return request(.renewalToken(refreshToken)) } } // MARK: - API enum AuthAPI { /// 토큰 재발급 case renewalToken(String) } extension AuthAPI: BaseAPI { var path: String { let apiPath = "/api-as/v1" switch self { case .renewalToken: return "\(apiPath)/\("renewalToken".lowercased())" } } var method: Moya.Method { switch self { case .renewalToken: return .post } } var task: Task { switch self { case let .renewalToken(refreshToken): return .requestParameters( parameters: ["refreshToken": refreshToken], encoding: JSONEncoding.default ) } } }