[Swift] Moya + renewal token

요청 결과가 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
      )
    }
  }
}