Gitodo!를 개발하면서 API 호출에 사용되는 client_id, client_secret과 같은 API키나 액세스 토큰 등의 정보가 코드에 공개되지 않도록 숨길 필요가 있었다. 그 과정을 기록한다 ...
API 키
API 키를 숨기는 방법으로는 xcconfig 파일을 사용했다. xcconfig 파일은 환경 변수처럼 값을 정의할 수 있어 간단하게 설정할 수 있다. Gitodo!는 Tuist를 사용하기 때문에 Project.swift를 구성하는 부분에 대해 작성해보겠다 !
1. xcconfig 파일 생성
xcconfig 파일은 Xcode 구성 설정 파일로, 빌드 설정을 관리할 수 있는 텍스트 파일이다. 간단하게 값을 key = value 형식으로 저장하면 된다. ( 이 파일은 .gitignore에 추가하여 Git 저장소에 올라가지 않도록 주의 ! )
CLIENT_ID = (비밀)
CLIENT_SECRET = (비밀)
2. info.Plist
info.Plist 파일은 애플리케이션의 설정 정보를 담고 있는 파일이다. 여기서 xcconfig 파일에 정의한 값을 환경 변수처럼 사용할 수 있다.
private let appInfoPlist = InfoPlist.extendingDefault(with: [
...
"CLIENT_ID": "$(CLIENT_ID)",
"CLIENT_SECRET": "$(CLIENT_SECRET)"
])
그렇다면 어떻게 xcconfig 파일에서 값을 가져올 수 있을까 ?
3. 타겟 설정
타겟 설정에서 settings 프로퍼티에 다음과 같이 정의해주면 된다. Debug, Release 모두 같은 client_id, client_secret 값을 사용하기 때문에 같은 파일을 지정해줬다.
.target(
...
settings: .settings(
configurations: [
.debug(name: "Debug", xcconfig: "Configurations/secrets.xcconfig"),
.release(name: "Release", xcconfig: "Configurations/secrets.xcconfig")
]
)
)
4. 사용해보기
정의한 키를 사용하려면 Bundle.main.object(forInfoDictionaryKey:)를 사용한다. Bundle 클래스는 애플리케이션의 리소스에 접근할 수 있게 해주고, object(forInfoDictionaryKey:) 메서드를 통해 info.plist에 정의된 값을 가져올 수 있다.
private var clientID: String {
return Bundle.main.object(forInfoDictionaryKey: "CLIENT_ID") as? String ?? ""
}
private var clientSecret: String {
return Bundle.main.object(forInfoDictionaryKey: "CLIENT_SECRET") as? String ?? ""
}
액세스 토큰
액세스 토큰은 OAuth 로그인을 통해 받은 임시 코드로 발급받기 때문에(앱 실행 중) xcconfig 파일에 정의하는 건 맞지 않다고 생각했다. 그래서 초기에는 UserDefaults에 저장했는데, 암호화되지 않은 상태로 저장을 하기 때문에 보안 문제가 있다고 판단하여 최종적으로는 KeyChain에 저장을 했다.
import Foundation
import Security
final class KeychainManager {
static let shared = KeychainManager()
private init() {}
private let service = Bundle.main.bundleIdentifier ?? "com.goorung.Gitodo"
private let account = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Gitodo"
// Create or Update
func save(key: String, data: String) {
if read(key: key) != nil {
update(key: key, data: data)
} else {
let dataToSave = data.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrGeneric as String: key,
kSecValueData as String: dataToSave
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
}
// Read
func read(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrGeneric as String: key,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: kCFBooleanTrue!
]
var item: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == noErr, let data = item as? Data, let result = String(data: data, encoding: .utf8) {
return result
}
return nil
}
// Update
func update(key: String, data: String) {
let newData = data.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrGeneric as String: key
]
let attributes: [String: Any] = [kSecValueData as String: newData]
SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
}
// Delete
func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrGeneric as String: key
]
SecItemDelete(query as CFDictionary)
}
}
사용 예시 :
// 저장
KeychainManager.shared.save(key: "accessToken", data: token.accessToken)
// 읽기
let accessToken = KeychainManager.shared.read(key: "accessToken")
'iOS 🍎 Swift' 카테고리의 다른 글
Xcode 메모리 누수 체크하기 (2) | 2024.08.05 |
---|---|
Github OAuth 앱 스토어 심사 (iOS에서 URL을 여는 방법) (8) | 2024.07.18 |
타겟 간 같은 Realm 저장소 사용하기 (feat. Tuist) (0) | 2024.07.18 |
iOS/Swift 토스트 메시지 🍞 (2) | 2024.07.16 |
Tuist 환경에서 Xcode Cloud CI/CD 구축 (1) | 2024.07.15 |