You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何向iOS快捷指令传递认证令牌?App Intents与Cognito方案咨询

基于App Intents+Cognito的认证令牌处理最佳实践与实现指导

一、令牌存储的安全规范

  • 绝对不要把access token、refresh token明文存在UserDefaults里,必须用Keychain Services存储,这是iOS/macOS下敏感数据存储的标准方案,能利用系统级加密保护令牌不被泄露。
  • 存储时设置合适的访问控制:比如指定kSecAttrAccessibleWhenUnlockedThisDeviceOnly,确保只有设备解锁且当前用户能访问,避免备份到iCloud或被其他应用读取。
  • 给每个令牌设置唯一的Keychain条目标识,比如com.yourapp.cognito.access-token、com.yourapp.cognito.refresh-token、com.yourapp.cognito.user-id,方便后续精准读取。

二、主应用向App Intents传递令牌的实现

App Intents运行在独立进程,无法直接访问主应用内存数据,需通过Keychain共享实现:

  1. 配置App Groups:在开发者后台和Xcode项目中开启同一个App Group(比如group.com.yourapp.shared),主应用和App Intents扩展都要加入该组。
  2. 修改Keychain存储代码:存储令牌时指定kSecAttrAccessGroup为你的App Group标识,让扩展能访问同一组的Keychain条目。
  3. 在App Intents中读取令牌:编写通用的Keychain读取工具类,直接从共享Keychain中获取user ID、access token和refresh token。

三、Cognito令牌的自动刷新逻辑

access token有效期通常为1小时,必须在App Intents中处理过期刷新:

  • 每次提交数据前,解析JWT的exp字段检查access token是否过期。
  • 若令牌过期,调用Cognito的initiateAuth接口,用refresh token换取新的access token和refresh token,成功后更新Keychain中的令牌。
  • 封装刷新逻辑为异步方法,确保数据提交前完成令牌更新,避免因过期导致请求失败。

四、App Intents中数据提交的安全处理

  • 所有Web服务通信必须使用HTTPS,禁止HTTP传输,防止令牌被窃听。
  • 提交请求时,将access token放在HTTP请求头的Authorization字段,格式为Bearer <access-token>,遵循OAuth2标准授权方式。
  • 处理请求失败场景:收到401 Unauthorized响应时,立即触发refresh token刷新流程并重试请求;若刷新失败(如refresh token过期),提示用户回到主应用重新登录。

五、代码示例片段

Keychain存储工具类(主应用与扩展通用)

import Security

enum KeychainError: Error {
    case noData
    case unexpectedData
    case unhandledError(status: OSStatus)
}

class KeychainManager {
    private let accessGroup = "group.com.yourapp.shared"
    private let tokenKeys = [
        "userId": "com.yourapp.cognito.user-id",
        "accessToken": "com.yourapp.cognito.access-token",
        "refreshToken": "com.yourapp.cognito.refresh-token"
    ]
    
    func saveToken(value: String, forKey key: String) throws {
        guard let keyIdentifier = tokenKeys[key] else { return }
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: keyIdentifier,
            kSecAttrAccessGroup: accessGroup,
            kSecValueData: value.data(using: .utf8)!
        ]
        
        SecItemDelete(query as CFDictionary)
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            throw KeychainError.unhandledError(status: status)
        }
    }
    
    func getToken(forKey key: String) throws -> String {
        guard let keyIdentifier = tokenKeys[key] else { throw KeychainError.noData }
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: keyIdentifier,
            kSecAttrAccessGroup: accessGroup,
            kSecReturnData: kCFBooleanTrue!,
            kSecMatchLimit: kSecMatchLimitOne
        ]
        
        var data: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &data)
        guard status == errSecSuccess else {
            throw KeychainError.unhandledError(status: status)
        }
        guard let dataValue = data as? Data, let token = String(data: dataValue, encoding: .utf8) else {
            throw KeychainError.unexpectedData
        }
        return token
    }
}

App Intents中令牌刷新与数据提交

import AppIntents

struct SubmitDataIntent: AppIntent {
    static var title: LocalizedStringResource = "提交用户数据"
    static var description: IntentDescription = IntentDescription("收集并提交用户数据到Web服务")
    
    @Parameter(title: "用户数据")
    var userData: String
    
    func perform() async throws -> some IntentResult {
        let keychain = KeychainManager()
        let userId = try keychain.getToken(forKey: "userId")
        var accessToken = try keychain.getToken(forKey: "accessToken")
        let refreshToken = try keychain.getToken(forKey: "refreshToken")
        
        // 检查access token是否过期
        if isTokenExpired(accessToken) {
            // 刷新令牌
            let newTokens = try await refreshCognitoToken(refreshToken: refreshToken)
            accessToken = newTokens.accessToken
            try keychain.saveToken(value: accessToken, forKey: "accessToken")
            try keychain.saveToken(value: newTokens.refreshToken, forKey: "refreshToken")
        }
        
        // 提交数据到Web服务
        try await submitDataToServer(userId: userId, accessToken: accessToken, data: userData)
        
        return .result(dialog: "数据提交成功")
    }
    
    private func isTokenExpired(_ token: String) -> Bool {
        // 解析JWT的exp字段判断过期状态
        guard let payload = token.split(separator: ".").dropFirst().first else { return true }
        guard let data = Data(base64Encoded: payload + "=="), 
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], 
              let exp = json["exp"] as? TimeInterval else {
            return true
        }
        return Date().timeIntervalSince1970 > exp
    }
    
    private func refreshCognitoToken(refreshToken: String) async throws -> (accessToken: String, refreshToken: String) {
        // 调用Cognito令牌刷新接口
        let url = URL(string: "https://your-cognito-domain/oauth2/token")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        let body = "grant_type=refresh_token&client_id=your-client-id&refresh_token=\(refreshToken)"
        request.httpBody = body.data(using: .utf8)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw NSError(domain: "TokenRefresh", code: -1, userInfo: [NSLocalizedDescriptionKey: "令牌刷新失败"])
        }
        let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
        guard let newAccessToken = json?["access_token"] as? String, 
              let newRefreshToken = json?["refresh_token"] as? String else {
            throw NSError(domain: "TokenRefresh", code: -2, userInfo: [NSLocalizedDescriptionKey: "令牌解析失败"])
        }
        return (newAccessToken, newRefreshToken)
    }
    
    private func submitDataToServer(userId: String, accessToken: String, data: String) async throws {
        let url = URL(string: "https://your-web-service.com/submit")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        let payload = ["userId": userId, "data": data]
        request.httpBody = try JSONSerialization.data(withJSONObject: payload)
        
        let (_, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse, 
              httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
            throw NSError(domain: "DataSubmit", code: -1, userInfo: [NSLocalizedDescriptionKey: "数据提交失败"])
        }
    }
}

内容的提问来源于stack exchange,提问作者Kittovski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 14:45:41