添加token管理

This commit is contained in:
2025-10-31 16:06:54 +08:00
parent 59d04bb33c
commit 90c1e7ff6c
10 changed files with 299 additions and 5 deletions

56
Shared/KBAuthManager.h Normal file
View File

@@ -0,0 +1,56 @@
//
// KBAuthManager.h
// 主 App 与键盘扩展共享使用
//
// 通过 Keychain Sharing 统一管理用户登录态access/refresh token
// 线程安全;在保存/清空时同时发送进程内通知与 Darwin 跨进程通知,
// 以便键盘扩展正运行在其他 App 时也能及时感知变更。
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// Darwin 跨进程通知名称:当令牌更新或清除时发送,用于提示 App/扩展刷新缓存。
extern NSString * const kKBDarwinAuthChanged;
/// 进程内通知NSNotificationCenter令牌更新或清除时发送。
extern NSNotificationName const KBAuthChangedNotification;
/// 简单的会话容器;可按需扩展字段。
@interface KBAuthSession : NSObject <NSSecureCoding>
@property (nonatomic, copy, nullable) NSString *accessToken;
@property (nonatomic, copy, nullable) NSString *refreshToken;
@property (nonatomic, strong, nullable) NSDate *expiryDate; // 可选:过期时间
@property (nonatomic, copy, nullable) NSString *userIdentifier; // 可选:如“用 Apple 登录”的 userIdentifier
@end
/// 基于“共享钥匙串”的鉴权管理器(使用 Keychain Sharing 访问组)。
@interface KBAuthManager : NSObject
+ (instancetype)shared;
/// 当前会话(内存缓存),在加载/保存/清除后更新。
@property (atomic, strong, readonly, nullable) KBAuthSession *current;
/// 是否已登录:存在 accessToken 且未明显过期(未设置过期时间则只要有 token 即视为已登录)。
- (BOOL)isLoggedIn;
/// 从钥匙串加载到内存;通常首次访问时会自动加载。
- (void)reloadFromKeychain;
/// 保存令牌到“共享钥匙串”并通知观察者。
- (BOOL)saveAccessToken:(NSString *)accessToken
refreshToken:(nullable NSString *)refreshToken
expiryDate:(nullable NSDate *)expiryDate
userIdentifier:(nullable NSString *)userIdentifier;
/// 从钥匙串与内存中清除令牌,并通知观察者。
- (void)signOut;
/// 便捷方法:若存在有效令牌,返回 `Authorization` 请求头;否则返回空字典。
- (NSDictionary<NSString *, NSString *> *)authorizationHeader;
@end
NS_ASSUME_NONNULL_END

188
Shared/KBAuthManager.m Normal file
View File

@@ -0,0 +1,188 @@
//
// KBAuthManager.m
//
//
// - 使 service/account KBAuthSession
// - kSecAttrAccessGroup Keychain Sharing 访 App
// - / Darwin 便
//
#import "KBAuthManager.h"
#import <Security/Security.h>
#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;
}
- (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<NSString *,NSString *> *)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);
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 (status != errSecSuccess || !dataRef) return nil;
return (__bridge_transfer NSData *)dataRef;
}
- (void)keychainDelete {
NSDictionary *query = [self baseKCQuery];
SecItemDelete((__bridge CFDictionaryRef)query);
}
@end

View File

@@ -1,8 +1,8 @@
//
// KBConfig.h
// Shared config/macros for both main app and keyboard extension.
// 主 App 与键盘扩展共用的配置/宏。
//
// Edit these values once and both targets pick them up via PCH import.
// 在此处修改后,会通过 PCH 被两个 target 同步引用。
//
#ifndef KBConfig_h
@@ -23,3 +23,11 @@
#endif /* KBConfig_h */
// --- 认证/共享钥匙串 配置 ---
// 若已在 Capabilities 中启用 Keychain Sharing并添加访问组
// $(AppIdentifierPrefix)com.keyBoardst.shared
// 运行时会展开为TN6HHV45BB.com.keyBoardst.shared
// KBAuthManager 通过下面的宏定位访问组;如需修改,可在 Build Settings 或前缀头中覆盖该宏。
#ifndef KB_KEYCHAIN_ACCESS_GROUP
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared"
#endif