美文网首页
Authentication Services框架详细解析 (八

Authentication Services框架详细解析 (八

作者: 刀客传奇 | 来源:发表于2021-04-03 10:03 被阅读0次

版本记录

版本号 时间
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,感兴趣的给个赞或者关注~~~

相关文章

网友评论

      本文标题:Authentication Services框架详细解析 (八

      本文链接:https://www.haomeiwen.com/subject/uptfkltx.html