版本记录
版本号 | 时间 |
---|---|
V1.0 | 2021.04.03 星期六 |
前言
Authentication Services
框架为用户提供了授权身份认证Authentication
服务,使用户更容易登录App
和服务。下面我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. Authentication Services框架详细解析 (一) —— 基本概览(一)
2. Authentication Services框架详细解析 (二) —— 使用Sign in with Apple实现用户身份验证(一)
3. Authentication Services框架详细解析 (三) —— 密码的自动填充(一)
4. Authentication Services框架详细解析 (四) —— 使用Account Authentication Modification Extension提升账号安全(一)
5. Authentication Services框架详细解析 (五) —— 使用web authentication session对App中的用户进行身份验证(一)
6. Authentication Services框架详细解析 (六) —— Web Browser App中支持Single Sign-On(一)
7. Authentication Services框架详细解析 (七) —— 使用ASWebAuthenticationSession实现OAuth(一)
源码
1. Swift
首先我们一起看一下工程组织结构:
下面就是源码了
1. RepositoriesViewModel.swift
import SwiftUI
class RepositoriesViewModel: ObservableObject {
@Published private(set) var repositories: [Repository]
let username: String
init() {
self.repositories = []
self.username = NetworkRequest.username ?? ""
}
private init(
repositories: [Repository],
username: String
) {
self.repositories = repositories
self.username = username
}
func load() {
NetworkRequest
.RequestType
.getRepos
.networkRequest()?
.start(responseType: [Repository].self) { [weak self] result in
switch result {
case .success(let networkResponse):
DispatchQueue.main.async {
self?.repositories = networkResponse.object
}
case .failure(let error):
print("Failed to get the user's repositories: \(error)")
}
}
}
func signOut() {
NetworkRequest.signOut()
}
static func preview() -> RepositoriesViewModel {
let repositories: [Repository] = [
Repository(id: 1, name: "First"),
Repository(id: 2, name: "Second"),
Repository(id: 3, name: "Third")
]
return RepositoriesViewModel(
repositories: repositories,
username: "GitHub user")
}
}
2. SignInViewModel.swift
import AuthenticationServices
import SwiftUI
class SignInViewModel: NSObject, ObservableObject {
@Published var isShowingRepositoriesView = false
@Published private(set) var isLoading = false
func signInTapped() {
guard let signInURL =
NetworkRequest.RequestType.signIn.networkRequest()?.url
else {
print("Could not create the sign in URL .")
return
}
let callbackURLScheme = NetworkRequest.callbackURLScheme
let authenticationSession = ASWebAuthenticationSession(
url: signInURL,
callbackURLScheme: callbackURLScheme) { [weak self] callbackURL, error in
// 1
guard
error == nil,
let callbackURL = callbackURL,
// 2
let queryItems = URLComponents(string: callbackURL.absoluteString)?.queryItems,
// 3
let code = queryItems.first(where: { $0.name == "code" })?.value,
// 4
let networkRequest =
NetworkRequest.RequestType.codeExchange(code: code).networkRequest()
else {
// 5
print("An error occurred when attempting to sign in.")
return
}
self?.isLoading = true
networkRequest.start(responseType: String.self) { result in
switch result {
case .success:
self?.getUser()
case .failure(let error):
print("Failed to exchange access code for tokens: \(error)")
self?.isLoading = false
}
}
}
authenticationSession.presentationContextProvider = self
authenticationSession.prefersEphemeralWebBrowserSession = true
if !authenticationSession.start() {
print("Failed to start ASWebAuthenticationSession")
}
}
func appeared() {
// Try to get the user in case the tokens are already stored on this device
getUser()
}
private func getUser() {
isLoading = true
NetworkRequest
.RequestType
.getUser
.networkRequest()?
.start(responseType: User.self) { [weak self] result in
switch result {
case .success:
self?.isShowingRepositoriesView = true
case .failure(let error):
print("Failed to get user, or there is no valid/active session: \(error.localizedDescription)")
}
self?.isLoading = false
}
}
}
extension SignInViewModel: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession)
-> ASPresentationAnchor {
let window = UIApplication.shared.windows.first { $0.isKeyWindow }
return window ?? ASPresentationAnchor()
}
}
3. RepositoriesView.swift
import SwiftUI
struct RepositoriesView: View {
@ObservedObject private var viewModel = RepositoriesViewModel()
@Binding private var displayed: Bool
init(
viewModel: RepositoriesViewModel = RepositoriesViewModel(),
displayed: Binding<Bool>
) {
self.viewModel = viewModel
self._displayed = displayed
}
var body: some View {
VStack {
Text(viewModel.username)
.font(.system(size: 20))
.fontWeight(.semibold)
.padding()
List {
ForEach(viewModel.repositories) { repo in
Text(repo.name)
}
}
}
.navigationBarTitle("My Repositories", displayMode: .inline)
.navigationBarItems(leading: signOutButton)
.navigationBarBackButtonHidden(true)
.onAppear {
viewModel.load()
}
}
private var signOutButton: some View {
Button("Sign Out") {
viewModel.signOut()
displayed = false
}
}
}
struct RepositoriesView_Previews: PreviewProvider {
static var previews: some View {
RepositoriesView(
viewModel: RepositoriesViewModel.preview(),
displayed: .constant(true))
}
}
4. SignInView.swift
import SwiftUI
struct SignInView: View {
@ObservedObject private var viewModel = SignInViewModel()
var body: some View {
NavigationView {
VStack(spacing: 30) {
NavigationLink(
destination: RepositoriesView(displayed: $viewModel.isShowingRepositoriesView),
isActive: $viewModel.isShowingRepositoriesView
) { EmptyView() }
Image("rw-logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 200, alignment: .center)
if viewModel.isLoading {
ProgressView()
} else {
Button(action: { viewModel.signInTapped() }, label: {
Text("Sign In")
.font(Font.system(size: 24).weight(.semibold))
.foregroundColor(Color("rw-light"))
.padding(.horizontal, 50)
.padding(.vertical, 8)
})
.background(buttonBackground)
}
}
.navigationBarHidden(true)
.onAppear {
viewModel.appeared()
}
}
}
private var buttonBackground: some View {
RoundedRectangle(cornerRadius: 8)
.fill(Color("rw-green"))
}
}
struct SignInView_Previews: PreviewProvider {
static var previews: some View {
SignInView()
}
}
5. NetworkRequest.swift
import Foundation
struct NetworkRequest {
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
enum RequestError: Error {
case invalidResponse
case networkCreationError
case otherError
case sessionExpired
}
enum RequestType: Equatable {
case codeExchange(code: String)
case getRepos
case getUser
case signIn
func networkRequest() -> NetworkRequest? {
guard let url = url() else {
return nil
}
return NetworkRequest(method: httpMethod(), url: url)
}
private func httpMethod() -> NetworkRequest.HTTPMethod {
switch self {
case .codeExchange:
return .post
case .getRepos:
return .get
case .getUser:
return .get
case .signIn:
return .get
}
}
private func url() -> URL? {
switch self {
case .codeExchange(let code):
let queryItems = [
URLQueryItem(name: "client_id", value: NetworkRequest.clientID),
URLQueryItem(name: "client_secret", value: NetworkRequest.clientSecret),
URLQueryItem(name: "code", value: code)
]
return urlComponents(host: "github.com", path: "/login/oauth/access_token", queryItems: queryItems).url
case .getRepos:
guard
let username = NetworkRequest.username,
!username.isEmpty
else {
return nil
}
return urlComponents(path: "/users/\(username)/repos", queryItems: nil).url
case .getUser:
return urlComponents(path: "/user", queryItems: nil).url
case .signIn:
let queryItems = [
URLQueryItem(name: "client_id", value: NetworkRequest.clientID)
]
return urlComponents(host: "github.com", path: "/login/oauth/authorize", queryItems: queryItems).url
}
}
private func urlComponents(host: String = "api.github.com", path: String, queryItems: [URLQueryItem]?) -> URLComponents {
switch self {
default:
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = host
urlComponents.path = path
urlComponents.queryItems = queryItems
return urlComponents
}
}
}
typealias NetworkResult<T: Decodable> = (response: HTTPURLResponse, object: T)
// MARK: Private Constants
static let callbackURLScheme = "YOUR_CALLBACK_SCHEME_HERE"
static let clientID = "YOUR_CLIENT_ID_HERE"
static let clientSecret = "YOUR_CLIENT_SECRET_HERE"
// MARK: Properties
var method: HTTPMethod
var url: URL
// MARK: Static Methods
static func signOut() {
Self.accessToken = ""
Self.refreshToken = ""
Self.username = ""
}
// MARK: Methods
func start<T: Decodable>(responseType: T.Type, completionHandler: @escaping ((Result<NetworkResult<T>, Error>) -> Void)) {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
if let accessToken = NetworkRequest.accessToken {
request.setValue("token \(accessToken)", forHTTPHeaderField: "Authorization")
}
let session = URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response as? HTTPURLResponse else {
DispatchQueue.main.async {
completionHandler(.failure(RequestError.invalidResponse))
}
return
}
guard
error == nil,
let data = data
else {
DispatchQueue.main.async {
let error = error ?? NetworkRequest.RequestError.otherError
completionHandler(.failure(error))
}
return
}
if T.self == String.self, let responseString = String(data: data, encoding: .utf8) {
let components = responseString.components(separatedBy: "&")
var dictionary: [String: String] = [:]
for component in components {
let itemComponents = component.components(separatedBy: "=")
if let key = itemComponents.first, let value = itemComponents.last {
dictionary[key] = value
}
}
DispatchQueue.main.async {
NetworkRequest.accessToken = dictionary["access_token"]
NetworkRequest.refreshToken = dictionary["refresh_token"]
// swiftlint:disable:next force_cast
completionHandler(.success((response, "Success" as! T)))
}
return
} else if let object = try? JSONDecoder().decode(T.self, from: data) {
DispatchQueue.main.async {
if let user = object as? User {
NetworkRequest.username = user.login
}
completionHandler(.success((response, object)))
}
return
} else {
DispatchQueue.main.async {
completionHandler(.failure(NetworkRequest.RequestError.otherError))
}
}
}
session.resume()
}
}
6. NetworkRequest+User.swift
import Foundation
extension NetworkRequest {
// MARK: Private Constants
private static let accessTokenKey = "accessToken"
private static let refreshTokenKey = "refreshToken"
private static let usernameKey = "username"
// MARK: Properties
static var accessToken: String? {
get {
UserDefaults.standard.string(forKey: accessTokenKey)
}
set {
UserDefaults.standard.setValue(newValue, forKey: accessTokenKey)
}
}
static var refreshToken: String? {
get {
UserDefaults.standard.string(forKey: refreshTokenKey)
}
set {
UserDefaults.standard.setValue(newValue, forKey: refreshTokenKey)
}
}
static var username: String? {
get {
UserDefaults.standard.string(forKey: usernameKey)
}
set {
UserDefaults.standard.setValue(newValue, forKey: usernameKey)
}
}
}
7. Repository.swift
import Foundation
struct Repository: Decodable, Identifiable {
var id: Int
var name: String
}
8. User.swift
import Foundation
struct User: Decodable {
var login: String
var name: String
}
9. AppMain.swift
import SwiftUI
@main
struct AppMain: App {
var body: some Scene {
WindowGroup {
SignInView()
}
}
}
后记
本篇主要讲述了使用
ASWebAuthenticationSession
实现OAuth
,感兴趣的给个赞或者关注~~~
网友评论