새로운 프로젝트를 진행하면서 깃허브 로그인을 통해 로그인하고, 로그인한 사용자의 정보와 레포지토리 정보를 받아와야 하는 기능을 구현하게 되었다.
먼저, 깃허브에 내 앱을 등록하여 ClientId와 Client secrets을 발급 받는다.
아래 다음과 같이 적어준다.
** callback URL에 내앱이름://login으로 적어주어야 정상적으로 작동한다.
URL Types에서 URL Schemes에 내 앱이름을 적어준다.
조금 다른점이 있지만 전체 로직이다. 좀 특이한 점은 Client에서 Provider로 code 요청, 받은 코드로 token을 요청해서 2번 요청한다. code를 받음으로써 인가된 사용자임을 인증받고, 그 다음 내가 원하는 정보인 access token을 받는다.
로그인을 하는데 있어서 2번의 요청이 번거로울 수 있는데, 이는 implicit grant를 사용하면 code요청 없이 한번의 요청으로 access token을 받기 때문이다. 그러나 이 implicit grant도 단점이 있는데, refresh token을 사용할 수 없어 access token의 발급 수명이 짧아져 만료될 때 마다 access token을 새로 요청해야 한다.
앱에서는 키체인과 같이 클라이언트에서 토큰 보관을 안전하게 할 수 있어 implicit grant 방식을 사용하지 않고, 보통 브라우저에서 사용한다.
브라우저는 데이터를 안전하게 관리할 수 있는 방법이 적어 정보를 최소한으로 노출시키기 위해 code호출을 생략하고 바로 access token을 받아 정보 노출을 간소화한다.
대신 access token의 수명을 짧게해 그나마 안전하게 유지하도록 한다.
전체코드
LoginManager.swift
import Foundation
import KeychainSwift
import Alamofire
import UIKit
class LoginManager {
static let shared = LoginManager()
private let client_id: String = ""
private let client_secret: String = ""
private let scope: String = "repo gist user"
private let githubURL: String = "https://github.com"
private let githubApiURL: String = "https://api.github.com"
func requestCode() {
var components = URLComponents(string: githubURL+ApiPath.LOGIN.rawValue)!
components.queryItems = [
URLQueryItem(name: "client_id", value: self.client_id),
URLQueryItem(name: "scope", value: self.scope),
]
let urlString = components.url?.absoluteString
if let url = URL(string: urlString!), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
func requestAccessToken(with code: String) {
let parameters = ["client_id": client_id,
"client_secret": client_secret,
"code": code]
let headers: HTTPHeaders = ["Accept": "application/json"]
AF.request(githubURL+ApiPath.ACCESS_TOKEN.rawValue,
method: .post, parameters: parameters,
headers: headers).responseJSON { (response) in
switch response.result {
case let .success(json):
if let dic = json as? [String: String] {
let accessToken = dic["access_token"] ?? ""
KeychainSwift().set(accessToken, forKey: "accessToken")
}
case let .failure(error):
print(error)
}
}
}
func getUser() {
let accessToken = KeychainSwift().get("accessToken") ?? ""
let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
"Authorization": "token \(accessToken)"]
AF.request(githubApiURL+ApiPath.USER.rawValue,
method: .get,
parameters: [:],
headers: headers).responseJSON(completionHandler: { (response) in
switch response.result {
case .success(let json):
print(json as! [String: Any])
case .failure:
print("")
}
})
}
func getRepos() {
let accessToken = KeychainSwift().get("accessToken") ?? ""
let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
"Authorization": "token \(accessToken)"]
AF.request(githubApiURL+ApiPath.REPOS.rawValue,
method: .get, parameters: [:],
headers: headers).responseJSON(completionHandler: { (response) in
switch response.result {
case .success(let json):
print(json)
case .failure:
print("")
}
})
}
func logout() {
KeychainSwift().clear()
}
}
ApiPath.swift
import Foundation
enum ApiPath: String {
case LOGIN = "/login/oauth/authorize" // 사용자의 깃허브 아이디 (사파리 페이지 이동)
case ACCESS_TOKEN = "/login/oauth/access_token" // access token 요청
case USER = "/user" // 유저 정보
case REPOS = "/user/repos" // 유저 레포지토리 정보
}
SceneDelegate.swift
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url {
let code = url.absoluteString.components(separatedBy: "code=").last ?? ""
LoginManager.shared.requestAccessToken(with: code)
}
}
(github documentation 참고)
1) 로그인 요청
사용자가 Github Login 버튼을 클릭했을때
@IBAction func login(_ sender: Any) {
LoginManager.shared.requestCode()
}
func requestCode() {
var components = URLComponents(string: githubURL+ApiPath.LOGIN.rawValue)!
components.queryItems = [
URLQueryItem(name: "client_id", value: self.client_id),
URLQueryItem(name: "scope", value: self.scope),
]
let urlString = components.url?.absoluteString
if let url = URL(string: urlString!), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
2) code 요청
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url {
let code = url.absoluteString.components(separatedBy: "code=").last ?? ""
LoginManager.shared.requestAccessToken(with: code)
}
}
3) 받은 코드로 access token 요청
func requestAccessToken(with code: String) {
let parameters = ["client_id": client_id,
"client_secret": client_secret,
"code": code]
let headers: HTTPHeaders = ["Accept": "application/json"]
AF.request(githubURL+ApiPath.ACCESS_TOKEN.rawValue,
method: .post, parameters: parameters,
headers: headers).responseJSON { (response) in
switch response.result {
case let .success(json):
if let dic = json as? [String: String] {
let accessToken = dic["access_token"] ?? ""
KeychainSwift().set(accessToken, forKey: "accessToken")
}
case let .failure(error):
print(error)
}
}
}
+추가) 받은 access token으로 user와 repo정보 요청
func getUser() {
let accessToken = KeychainSwift().get("accessToken") ?? ""
let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
"Authorization": "token \(accessToken)"]
AF.request(githubApiURL+ApiPath.USER.rawValue,
method: .get,
parameters: [:],
headers: headers).responseJSON(completionHandler: { (response) in
switch response.result {
case .success(let json):
print(json as! [String: Any])
case .failure:
print("")
}
})
}
func getRepos() {
let accessToken = KeychainSwift().get("accessToken") ?? ""
let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
"Authorization": "token \(accessToken)"]
AF.request(githubApiURL+ApiPath.REPOS.rawValue,
method: .get, parameters: [:],
headers: headers).responseJSON(completionHandler: { (response) in
switch response.result {
case .success(let json):
print(json)
case .failure:
print("")
}
})
}
참고
https://eunjin3786.tistory.com/211
https://zeddios.tistory.com/1102
'iOS' 카테고리의 다른 글
[iOS] 디스패치큐(GCD)의 종류와 특성 (메인큐, 글로벌큐, 프라이빗큐) (0) | 2022.12.07 |
---|---|
[iOS] GCD와 Operation, 동기와 비동기, 직렬과 동시 (1) | 2022.12.05 |
iOS 개인정보 처리방침 (0) | 2022.09.25 |
[iOS] Apple developer 개발자 팀 등록 (Certificates, identifiers & Profiles가 나타나지 않을 때) (0) | 2022.04.17 |
[iOS/xcode] 오류해결 : this class is not key value coding-compliant for the key (0) | 2022.04.01 |