310 lines
10 KiB
Swift
310 lines
10 KiB
Swift
import SwiftUI
|
||
import AuthenticationServices // 苹果登录功能
|
||
import Alamofire // 网络请求
|
||
import CryptoKit // 加密功能
|
||
|
||
/// 主登录视图 - 处理苹果登录
|
||
struct LoginView: View {
|
||
// MARK: - 属性
|
||
@Environment(\.dismiss) private var dismiss // 用于关闭视图
|
||
@State private var isLoading = false // 加载状态
|
||
@State private var showError = false // 是否显示错误
|
||
@State private var errorMessage = "" // 错误信息
|
||
@State private var currentNonce: String? // 用于防止重放攻击的随机数
|
||
|
||
// MARK: - 视图主体
|
||
var body: some View {
|
||
ZStack {
|
||
// 背景
|
||
Color(.systemBackground)
|
||
.edgesIgnoringSafeArea(.all)
|
||
|
||
// 主要内容
|
||
VStack(spacing: 24) {
|
||
Spacer()
|
||
appHeaderView() // 应用标题
|
||
signInButton() // 登录按钮
|
||
Spacer()
|
||
termsAndPrivacyView() // 服务条款和隐私政策
|
||
}
|
||
.padding()
|
||
.alert(isPresented: $showError) {
|
||
Alert(
|
||
title: Text("Error"),
|
||
message: Text(errorMessage),
|
||
dismissButton: .default(Text("OK"))
|
||
)
|
||
}
|
||
|
||
// 加载指示器
|
||
if isLoading {
|
||
loadingView()
|
||
}
|
||
}
|
||
.navigationBarHidden(true)
|
||
}
|
||
|
||
// MARK: - 视图组件
|
||
|
||
/// 应用标题视图
|
||
private func appHeaderView() -> some View {
|
||
VStack(spacing: 16) {
|
||
Image(systemName: "person.circle.fill")
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fit)
|
||
.frame(width: 80, height: 80)
|
||
.foregroundColor(.blue)
|
||
|
||
Text("Welcome to Wake")
|
||
.font(.largeTitle)
|
||
.fontWeight(.bold)
|
||
|
||
Text("Sign in to continue")
|
||
.font(.subheadline)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.padding(.bottom, 40)
|
||
}
|
||
|
||
/// 苹果登录按钮
|
||
private func signInButton() -> some View {
|
||
SignInWithAppleButton(
|
||
onRequest: { request in
|
||
// 生成随机数用于安全验证
|
||
let nonce = String.randomURLSafeString(length: 32)
|
||
self.currentNonce = nonce
|
||
|
||
// 配置登录请求
|
||
request.requestedScopes = [.fullName, .email] // 请求用户全名和邮箱
|
||
request.nonce = self.sha256(nonce) // 设置nonce
|
||
},
|
||
onCompletion: handleAppleSignIn // 登录完成处理
|
||
)
|
||
.signInWithAppleButtonStyle(.black) // 按钮样式
|
||
.frame(height: 50)
|
||
.padding(.horizontal, 40)
|
||
.cornerRadius(10)
|
||
}
|
||
|
||
/// 服务条款和隐私政策链接
|
||
private func termsAndPrivacyView() -> some View {
|
||
VStack(spacing: 8) {
|
||
Text("By continuing, you agree to our")
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
|
||
HStack(spacing: 16) {
|
||
Button("Terms of Service") {
|
||
openURL("https://yourwebsite.com/terms")
|
||
}
|
||
.font(.caption)
|
||
.foregroundColor(.blue)
|
||
|
||
Text("•")
|
||
.foregroundColor(.secondary)
|
||
|
||
Button("Privacy Policy") {
|
||
openURL("https://yourwebsite.com/privacy")
|
||
}
|
||
.font(.caption)
|
||
.foregroundColor(.blue)
|
||
}
|
||
}
|
||
.padding(.bottom, 24)
|
||
}
|
||
|
||
/// 加载视图
|
||
private func loadingView() -> some View {
|
||
return ZStack {
|
||
Color.black.opacity(0.4)
|
||
.edgesIgnoringSafeArea(.all)
|
||
|
||
ProgressView()
|
||
.scaleEffect(1.5)
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
}
|
||
}
|
||
|
||
// MARK: - 苹果登录处理
|
||
|
||
/// 处理苹果登录结果
|
||
private func handleAppleSignIn(result: Result<ASAuthorization, Error>) {
|
||
print("🔵 [Apple Sign In] 开始处理登录结果...")
|
||
switch result {
|
||
case .success(let authResults):
|
||
print("✅ [Apple Sign In] 登录授权成功")
|
||
processAppleIDCredential(authResults.credential)
|
||
case .failure(let error):
|
||
print("❌ [Apple Sign In] 登录失败: \(error.localizedDescription)")
|
||
handleSignInError(error)
|
||
}
|
||
}
|
||
|
||
/// 处理苹果ID凭证
|
||
private func processAppleIDCredential(_ credential: ASAuthorizationCredential) {
|
||
print("🔵 [Apple ID] 开始处理凭证...")
|
||
guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else {
|
||
print("❌ [Apple ID] 凭证类型不匹配")
|
||
showError(message: "无法处理Apple ID凭证")
|
||
return
|
||
}
|
||
|
||
// 获取用户数据
|
||
let userId = appleIDCredential.user
|
||
let email = appleIDCredential.email ?? ""
|
||
let fullName = [
|
||
appleIDCredential.fullName?.givenName,
|
||
appleIDCredential.fullName?.familyName
|
||
]
|
||
.compactMap { $0 }
|
||
.joined(separator: " ")
|
||
|
||
print("ℹ️ [Apple ID] 用户数据 - ID: \(userId), 邮箱: \(email.isEmpty ? "未提供" : email), 姓名: \(fullName.isEmpty ? "未提供" : fullName)")
|
||
|
||
// 获取身份令牌
|
||
guard let identityTokenData = appleIDCredential.identityToken,
|
||
let identityToken = String(data: identityTokenData, encoding: .utf8) else {
|
||
print("❌ [Apple ID] 无法获取身份令牌")
|
||
showError(message: "无法获取身份令牌")
|
||
return
|
||
}
|
||
|
||
// 获取授权码(可选)
|
||
var authCode: String? = nil
|
||
if let authCodeData = appleIDCredential.authorizationCode {
|
||
authCode = String(data: authCodeData, encoding: .utf8)
|
||
print("ℹ️ [Apple ID] 获取到授权码")
|
||
} else {
|
||
print("ℹ️ [Apple ID] 未获取到授权码(可选)")
|
||
}
|
||
|
||
print("🔵 [Apple ID] 准备调用后端认证...")
|
||
authenticateWithBackend(
|
||
userId: userId,
|
||
email: email,
|
||
name: fullName,
|
||
identityToken: identityToken,
|
||
authCode: authCode
|
||
)
|
||
}
|
||
|
||
// MARK: - 网络操作
|
||
|
||
/// 与后端服务器进行认证
|
||
private func authenticateWithBackend(
|
||
userId: String,
|
||
email: String,
|
||
name: String,
|
||
identityToken: String,
|
||
authCode: String?
|
||
) {
|
||
isLoading = true
|
||
print("🔵 [Backend] 开始后端认证...")
|
||
|
||
let url = "https://your-api-endpoint.com/api/auth/apple"
|
||
var parameters: [String: Any] = [
|
||
"appleUserId": userId,
|
||
"email": email,
|
||
"name": name,
|
||
"identityToken": identityToken
|
||
]
|
||
|
||
// 添加授权码(如果存在)
|
||
if let authCode = authCode {
|
||
parameters["authorizationCode"] = authCode
|
||
}
|
||
|
||
print("📤 [Backend] 请求参数: \(parameters)")
|
||
|
||
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default)
|
||
.validate()
|
||
.responseJSON { response in
|
||
self.isLoading = false
|
||
|
||
switch response.result {
|
||
case .success(let value):
|
||
print("✅ [Backend] 认证成功: \(value)")
|
||
self.handleSuccessfulAuthentication()
|
||
case .failure(let error):
|
||
print("❌ [Backend] 认证失败: \(error.localizedDescription)")
|
||
if let data = response.data, let json = String(data: data, encoding: .utf8) {
|
||
print("❌ [Backend] 错误详情: \(json)")
|
||
}
|
||
self.handleAuthenticationError(error)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 辅助方法
|
||
|
||
/// 处理认证成功
|
||
private func handleSuccessfulAuthentication() {
|
||
print("✅ [Auth] 登录成功,准备关闭登录页面...")
|
||
DispatchQueue.main.async {
|
||
self.dismiss()
|
||
}
|
||
}
|
||
|
||
/// 处理登录错误
|
||
private func handleSignInError(_ error: Error) {
|
||
let errorMessage = (error as NSError).localizedDescription
|
||
print("❌ [Auth] 登录错误: \(errorMessage)")
|
||
showError(message: "登录失败: \(error.localizedDescription)")
|
||
}
|
||
|
||
/// 处理认证错误
|
||
private func handleAuthenticationError(_ error: AFError) {
|
||
let errorMessage = error.localizedDescription
|
||
print("❌ [Auth] 认证错误: \(errorMessage)")
|
||
showError(message: "登录失败: \(errorMessage)")
|
||
}
|
||
|
||
/// 显示错误信息
|
||
private func showError(message: String) {
|
||
DispatchQueue.main.async {
|
||
self.errorMessage = message
|
||
self.showError = true
|
||
}
|
||
}
|
||
|
||
/// 在Safari中打开URL
|
||
private func openURL(_ string: String) {
|
||
guard let url = URL(string: string) else { return }
|
||
UIApplication.shared.open(url)
|
||
}
|
||
|
||
/// SHA256哈希函数
|
||
private func sha256(_ input: String) -> String {
|
||
let inputData = Data(input.utf8)
|
||
let hashedData = SHA256.hash(data: inputData)
|
||
let hashString = hashedData.compactMap { String(format: "%02x", $0) }.joined()
|
||
return hashString
|
||
}
|
||
}
|
||
|
||
// MARK: - 字符串扩展:生成随机字符串
|
||
|
||
extension String {
|
||
/// 生成指定长度的随机URL安全字符串
|
||
/// - Parameter length: 字符串长度
|
||
/// - Returns: 随机字符串
|
||
static func randomURLSafeString(length: Int) -> String {
|
||
let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"
|
||
var randomString = ""
|
||
|
||
for _ in 0..<length {
|
||
let randomIndex = Int.random(in: 0..<characters.count)
|
||
let character = characters[characters.index(characters.startIndex, offsetBy: randomIndex)]
|
||
randomString.append(character)
|
||
}
|
||
|
||
return randomString
|
||
}
|
||
}
|
||
|
||
// MARK: - 预览
|
||
struct LoginView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
LoginView()
|
||
}
|
||
} |