iOS 팀 프로젝트/소분소분

AuthInterceptor 만들기

seongpil Heo 2025. 9. 24. 23:24

  👨🏻‍💻  오늘의 작업

[ 1. Utils 파일 내 전역 함수 및 변수 추가 ] 

 

문자열에서 날짜 계산하는 함수와 날짜에서 문자열 계산하는 함수를 추가하였다.

해당 함수는 액세스 토큰 만료가 되었는지 확인하기 위해서 사용할 예정이다.

 

API URL을 사용하는 곳이 많아짐에 따라 Utils 파일에 전역변수로 추가하였다.

 

[ 2. PublicAPI 파일 내 apiUrl 수정 ]

 

Bundle.main...으로 사용하던 apiUrl 주소를 Utils 파일에 있는 전역 변수인 API_URL로 변경

 

[ 3. AuthorizedAPI 추가 ]

 

추후에 사용할 Auth API를 위한 AuthorizedAPI 추가

 

[ 4. NetworkManager AuthProvider 추가 ]

 

[ 5. AuthInterceptor 추가 ]

//
//  AuthInterceptor.swift
//  SoBunSoBun
//
//  Created by 허성필 on 9/24/25.
//

import Foundation
import Alamofire
import Moya

final class AuthInterceptor: RequestInterceptor {

    static let shared = AuthInterceptor()

    private init() {}

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {

        // 저장된 액세스 토큰과 액세스 토큰 만료 시간을 가져오기
        guard let accessToken = KeyChain.shared.get(key: "ACCESS_TOKEN"),
              let accessTokenExpireAtKST = KeyChain.shared.get(key: "ACCESS_TOKEN_EXPIRE_AT_KST")
        else {
            // TODO: LogOut 구현하기 (ex 로그인 화면으로 이동)
            return
        }

        let now = Date()
        let isAccessExpired = stringToDate(string: accessTokenExpireAtKST,
                                           format: "yyyy-MM-dd HH:mm:ss") < now

        // 현재 시간과 accessToken의 만료 시간을 비교
        if isAccessExpired {
            completion(.failure(NSError(domain: "APIService", code: 401, userInfo: [NSLocalizedDescriptionKey: "액세스 토큰이 없음"])))
            return
        }

        // 헤더에 accessToken을 담아서 전달
        var urlRequest = urlRequest
        urlRequest.headers.add(.authorization(bearerToken: accessToken))

        completion(.success(urlRequest))
    }

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

        print("retry 진입")
        guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
            print("401 오류가 아님")
            completion(.doNotRetryWithError(error))
            return
        }

        guard let refreshTokenExpireAtKST = KeyChain.shared.get(key: "REFRESH_TOKEN_EXPIRE_AT_KST") else {
            // TODO: LogOut 구현하기 (ex 로그인 화면으로 이동)
            completion(.doNotRetry)
            print("리프레시 토큰 만료 시간 정보 없음")
            return
        }

        let now = Date()
        let isRefreshExpired = stringToDate(string: refreshTokenExpireAtKST,
                                            format: "yyyy-MM-dd HH:mm:ss") < now

        // 현재 시간과 refreshToken의 만료 시간을 비교
        if isRefreshExpired {
            // TODO: LogOut 구현하기 (ex 로그인 화면으로 이동)
            completion(.doNotRetry)
            print("리프레시 토큰 만료")
            return
        }

        if isRefreshing {
            print("재발급 중")
            completion(.retry)
        } else {
            isRefreshing = true

            refreshToken() { [weak self] isSuccess in
                guard let self = self else { return }
                isRefreshing = false

                if isSuccess {
                    print("액세스 토큰 재발급 완료")
                    completion(.retry)
                } else {
                    print("리프레시 토큰 만료")
                    // TODO: 로그아웃 구현 (ex 로그인 화면으로 넘기기)
                    completion(.doNotRetry)
                }
            }
        }
    }

    // 리프레시 토큰을 사용하여 액세스 토큰을 재발급 후 Keychain에 저장
    private func refreshToken(completion: @escaping(Bool) -> Void) {
        guard let refresh = KeyChain.shared.get(key: "REFRESH_TOKEN") else {
            print("리프레시 토큰 없음")
            completion(false)
            return
        }

        let parameters: [String: Any] = ["refresh": refresh]

        AF.request("\(API_URL)/auth/token/refresh",
                   method: .post,
                   parameters: parameters,
                   encoding: JSONEncoding.default)
        .validate(statusCode: 200..<300)
        .responseDecodable(of: UserModel.self) { response in
            switch response.result {
            case .success(let userModel):
                KeyChain.shared.set(key: "ACCESS_TOKEN", value: userModel.accessToken)
                KeyChain.shared.set(key: "ACCESS_TOKEN_EXPIRE_AT_KST", value: String(userModel.accessTokenExpiresAtKst))
                completion(true)
            case .failure(let error):
                print(error.localizedDescription)
                completion(false)
            }
        }
    }
}

 

토큰 만료 시간과 현재 시간을 비교해서 만료되었다면 재발급을 요청하고

재발급 만료 시간과 현재 시간을 비교해서 만료되었다면 로그아웃을 진행하는 AuthInterceptor 추가

 

'iOS 팀 프로젝트 > 소분소분' 카테고리의 다른 글

Components 만들기 #2  (0) 2025.10.03
Components 만들기 #1  (0) 2025.09.26
Localizable.xcstrings 줄 넘김 방법  (0) 2025.09.19
디자인 시스템 추가 적용  (0) 2025.09.18
디자인 시스템 추가  (0) 2025.09.17