// // KBAuthManager.m // // 关键点: // - 使用固定的 service/account 将 KBAuthSession 序列化后保存到钥匙串; // - 通过 kSecAttrAccessGroup 指定 Keychain Sharing 访问组,实现 App 与扩展共享; // - 保存/清除时发送 Darwin 跨进程通知,便于对端刷新缓存; // #import "KBAuthManager.h" #import #import "KBConfig.h" // 需要共享钥匙串访问组常量,见 KBConfig.h 中的说明 NSString * const kKBDarwinAuthChanged = @"com.keyBoardst.auth.changed"; NSNotificationName const KBAuthChangedNotification = @"KBAuthChangedNotification"; static NSString * const kKBKCService = @"com.keyBoardst.auth"; // 钥匙串 service 名 static NSString * const kKBKCAccount = @"session"; // 钥匙串 account 键 // 用于 Keychain Sharing 的访问组;必须与两个 target 的 entitlements 配置一致。 // 示例(Capabilities 中勾选 Keychain Sharing 后的值): // $(AppIdentifierPrefix)com.keyBoardst.shared // 运行时会被展开为:TN6HHV45BB.com.keyBoardst.shared #ifndef KB_KEYCHAIN_ACCESS_GROUP #define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared" #endif // 过期宽限:若过期时间距离当前 <= 该阈值,则视为已过期。 static const NSTimeInterval kKBExpiryGrace = 5.0; // 秒 @implementation KBAuthSession + (BOOL)supportsSecureCoding { return YES; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:self.accessToken forKey:@"accessToken"]; [coder encodeObject:self.refreshToken forKey:@"refreshToken"]; [coder encodeObject:self.expiryDate forKey:@"expiryDate"]; [coder encodeObject:self.userIdentifier forKey:@"userIdentifier"]; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super init]) { _accessToken = [coder decodeObjectOfClass:NSString.class forKey:@"accessToken"]; _refreshToken = [coder decodeObjectOfClass:NSString.class forKey:@"refreshToken"]; _expiryDate = [coder decodeObjectOfClass:NSDate.class forKey:@"expiryDate"]; _userIdentifier = [coder decodeObjectOfClass:NSString.class forKey:@"userIdentifier"]; } return self; } @end @interface KBAuthManager () @property (atomic, strong, readwrite, nullable) KBAuthSession *current; @end @implementation KBAuthManager + (instancetype)shared { static KBAuthManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBAuthManager new]; }); return m; } #if DEBUG static inline void KBLog(NSString *fmt, ...) { va_list args; va_start(args, fmt); NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:args]; va_end(args); NSLog(@"[KBAuth] %@", msg); } #endif - (instancetype)init { if (self = [super init]) { [self reloadFromKeychain]; // 监听 Darwin 跨进程通知(App 与扩展之间) CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), KBAuthDarwinCallback, (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, CFNotificationSuspensionBehaviorDeliverImmediately); } return self; } static void KBAuthDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) { KBAuthManager *self = (__bridge KBAuthManager *)observer; [self reloadFromKeychain]; } - (void)dealloc { CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL); } - (BOOL)isLoggedIn { KBAuthSession *s = self.current; if (s.accessToken.length == 0) return NO; if (!s.expiryDate) return YES; // 未设置过期时间时,只要有 token 即视为已登录 return ([s.expiryDate timeIntervalSinceNow] > kKBExpiryGrace); } #pragma mark - Public - (void)reloadFromKeychain { NSData *data = [self keychainRead]; KBAuthSession *session = nil; if (data.length > 0) { @try { session = [NSKeyedUnarchiver unarchivedObjectOfClass:KBAuthSession.class fromData:data error:NULL]; } @catch (__unused NSException *e) { session = nil; } } self.current = session; [[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil]; // 进程内通知 } - (BOOL)saveAccessToken:(NSString *)accessToken refreshToken:(NSString *)refreshToken expiryDate:(NSDate *)expiryDate userIdentifier:(NSString *)userIdentifier { KBAuthSession *s = [KBAuthSession new]; s.accessToken = accessToken ?: @""; s.refreshToken = refreshToken; s.expiryDate = expiryDate; s.userIdentifier = userIdentifier; NSError *err = nil; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:s requiringSecureCoding:YES error:&err]; if (err || data.length == 0) return NO; BOOL ok = [self keychainWrite:data]; if (ok) { self.current = s; // 进程内通知 [[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil]; // 跨进程通知(App <-> 扩展) CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true); } return ok; } - (void)signOut { [self keychainDelete]; self.current = nil; [[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil]; CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true); } - (NSDictionary *)authorizationHeader { NSString *t = self.current.accessToken; if (t.length == 0) return @{}; // 未登录返回空头部 return @{ @"Authorization": [@"Bearer " stringByAppendingString:t] }; } #pragma mark - Keychain (shared) - (NSMutableDictionary *)baseKCQuery { NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: kKBKCService, (__bridge id)kSecAttrAccount: kKBKCAccount } mutableCopy]; // 指定共享访问组(App 与扩展共用同一组) q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP; return q; } - (BOOL)keychainWrite:(NSData *)data { if (!data) return NO; NSMutableDictionary *query = [self baseKCQuery]; SecItemDelete((__bridge CFDictionaryRef)query); // 设置属性 query[(__bridge id)kSecValueData] = data; // 访问控制:设备首次解锁后可读,不随备份迁移 query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); #if DEBUG if (status != errSecSuccess) { KBLog(@"SecItemAdd failed status=%ld group=%@", (long)status, KB_KEYCHAIN_ACCESS_GROUP); } else { KBLog(@"SecItemAdd ok group=%@", KB_KEYCHAIN_ACCESS_GROUP); } #endif return (status == errSecSuccess); } - (NSData *)keychainRead { NSMutableDictionary *query = [self baseKCQuery]; query[(__bridge id)kSecReturnData] = @YES; query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; CFTypeRef dataRef = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef); #if DEBUG if (status != errSecSuccess) { KBLog(@"SecItemCopyMatching status=%ld group=%@ (item not found or no entitlement)", (long)status, KB_KEYCHAIN_ACCESS_GROUP); } else { KBLog(@"SecItemCopyMatching ok group=%@", KB_KEYCHAIN_ACCESS_GROUP); } #endif if (status != errSecSuccess || !dataRef) return nil; return (__bridge_transfer NSData *)dataRef; } - (void)keychainDelete { NSDictionary *query = [self baseKCQuery]; SecItemDelete((__bridge CFDictionaryRef)query); } @end