修改KBAuthSession主程序添加token extension没有拿到的情况
This commit is contained in:
@@ -62,6 +62,15 @@ static const NSTimeInterval kKBExpiryGrace = 5.0; // 秒
|
|||||||
return m;
|
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 {
|
- (instancetype)init {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
[self reloadFromKeychain];
|
[self reloadFromKeychain];
|
||||||
@@ -166,6 +175,13 @@ static void KBAuthDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||||
|
|
||||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
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);
|
return (status == errSecSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +192,13 @@ static void KBAuthDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
|
|
||||||
CFTypeRef dataRef = NULL;
|
CFTypeRef dataRef = NULL;
|
||||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef);
|
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;
|
if (status != errSecSuccess || !dataRef) return nil;
|
||||||
return (__bridge_transfer NSData *)dataRef;
|
return (__bridge_transfer NSData *)dataRef;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -847,6 +847,10 @@
|
|||||||
DEVELOPMENT_TEAM = TN6HHV45BB;
|
DEVELOPMENT_TEAM = TN6HHV45BB;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GCC_PREFIX_HEADER = CustomKeyboard/PrefixHeader.pch;
|
GCC_PREFIX_HEADER = CustomKeyboard/PrefixHeader.pch;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"KB_KEYCHAIN_ACCESS_GROUP=@\\\"$(AppIdentifierPrefix)com.keyBoardst.shared\\\"",
|
||||||
|
);
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = CustomKeyboard/Info.plist;
|
INFOPLIST_FILE = CustomKeyboard/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "我的输入法";
|
INFOPLIST_KEY_CFBundleDisplayName = "我的输入法";
|
||||||
@@ -876,6 +880,10 @@
|
|||||||
DEVELOPMENT_TEAM = TN6HHV45BB;
|
DEVELOPMENT_TEAM = TN6HHV45BB;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GCC_PREFIX_HEADER = CustomKeyboard/PrefixHeader.pch;
|
GCC_PREFIX_HEADER = CustomKeyboard/PrefixHeader.pch;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"KB_KEYCHAIN_ACCESS_GROUP=@\\\"$(AppIdentifierPrefix)com.keyBoardst.shared\\\"",
|
||||||
|
);
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = CustomKeyboard/Info.plist;
|
INFOPLIST_FILE = CustomKeyboard/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "我的输入法";
|
INFOPLIST_KEY_CFBundleDisplayName = "我的输入法";
|
||||||
@@ -906,6 +914,10 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = TN6HHV45BB;
|
DEVELOPMENT_TEAM = TN6HHV45BB;
|
||||||
GCC_PREFIX_HEADER = keyBoard/KeyBoardPrefixHeader.pch;
|
GCC_PREFIX_HEADER = keyBoard/KeyBoardPrefixHeader.pch;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"KB_KEYCHAIN_ACCESS_GROUP=@\\\"$(AppIdentifierPrefix)com.keyBoardst.shared\\\"",
|
||||||
|
);
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = keyBoard/Info.plist;
|
INFOPLIST_FILE = keyBoard/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
|
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
|
||||||
@@ -940,6 +952,10 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = TN6HHV45BB;
|
DEVELOPMENT_TEAM = TN6HHV45BB;
|
||||||
GCC_PREFIX_HEADER = keyBoard/KeyBoardPrefixHeader.pch;
|
GCC_PREFIX_HEADER = keyBoard/KeyBoardPrefixHeader.pch;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"KB_KEYCHAIN_ACCESS_GROUP=@\\\"$(AppIdentifierPrefix)com.keyBoardst.shared\\\"",
|
||||||
|
);
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = keyBoard/Info.plist;
|
INFOPLIST_FILE = keyBoard/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
|
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#import "HomeVC.h"
|
#import "HomeVC.h"
|
||||||
#import "MyVC.h"
|
#import "MyVC.h"
|
||||||
#import "BaseNavigationController.h"
|
#import "BaseNavigationController.h"
|
||||||
|
#import "KBAuthManager.h"
|
||||||
@interface BaseTabBarController ()
|
@interface BaseTabBarController ()
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
navMy.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"我的" image:nil selectedImage:nil];
|
navMy.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"我的" image:nil selectedImage:nil];
|
||||||
|
|
||||||
self.viewControllers = @[navHome, navMy];
|
self.viewControllers = @[navHome, navMy];
|
||||||
|
|
||||||
|
[[KBAuthManager shared] saveAccessToken:@"TEST" refreshToken:nil expiryDate:[NSDate dateWithTimeIntervalSinceNow:3600] userIdentifier:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
56
keyBoard/Class/Shared/KBAuthManager.h
Normal file
56
keyBoard/Class/Shared/KBAuthManager.h
Normal 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
|
||||||
211
keyBoard/Class/Shared/KBAuthManager.m
Normal file
211
keyBoard/Class/Shared/KBAuthManager.m
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
//
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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<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);
|
||||||
|
#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
|
||||||
33
keyBoard/Class/Shared/KBConfig.h
Normal file
33
keyBoard/Class/Shared/KBConfig.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// KBConfig.h
|
||||||
|
// 主 App 与键盘扩展共用的配置/宏。
|
||||||
|
//
|
||||||
|
// 在此处修改后,会通过 PCH 被两个 target 同步引用。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef KBConfig_h
|
||||||
|
#define KBConfig_h
|
||||||
|
|
||||||
|
// 基础baseUrl
|
||||||
|
#ifndef KB_BASE_URL
|
||||||
|
#define KB_BASE_URL @"https://m1.apifoxmock.com/m1/5438099-5113192-default/"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Universal Links 通用链接
|
||||||
|
#ifndef KB_UL_BASE
|
||||||
|
#define KB_UL_BASE @"https://your.domain/ul"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||||
|
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||||
|
|
||||||
|
#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
|
||||||
56
keyBoard/Shared/KBAuthManager.h
Normal file
56
keyBoard/Shared/KBAuthManager.h
Normal 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
|
||||||
211
keyBoard/Shared/KBAuthManager.m
Normal file
211
keyBoard/Shared/KBAuthManager.m
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
//
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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<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);
|
||||||
|
#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
|
||||||
33
keyBoard/Shared/KBConfig.h
Normal file
33
keyBoard/Shared/KBConfig.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// KBConfig.h
|
||||||
|
// 主 App 与键盘扩展共用的配置/宏。
|
||||||
|
//
|
||||||
|
// 在此处修改后,会通过 PCH 被两个 target 同步引用。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef KBConfig_h
|
||||||
|
#define KBConfig_h
|
||||||
|
|
||||||
|
// 基础baseUrl
|
||||||
|
#ifndef KB_BASE_URL
|
||||||
|
#define KB_BASE_URL @"https://m1.apifoxmock.com/m1/5438099-5113192-default/"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Universal Links 通用链接
|
||||||
|
#ifndef KB_UL_BASE
|
||||||
|
#define KB_UL_BASE @"https://your.domain/ul"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||||
|
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||||
|
|
||||||
|
#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
|
||||||
Reference in New Issue
Block a user