1
This commit is contained in:
32
Shared/KBSignUtils.h
Normal file
32
Shared/KBSignUtils.h
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// KBSignUtils.h
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2025/12/4.
|
||||
//
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBSignUtils : NSObject
|
||||
|
||||
/// URL 编码(与后端保持一致)
|
||||
+ (NSString *)urlEncode:(NSString *)value;
|
||||
|
||||
/// HMAC-SHA256,返回十六进制小写字符串
|
||||
+ (NSString *)hmacSHA256:(NSString *)data secret:(NSString *)secret;
|
||||
|
||||
/// 生成签名:传入参与签名的所有参数(不含 sign 自身)
|
||||
+ (NSString *)signWithParams:(NSDictionary<NSString *, NSString *> *)params
|
||||
secret:(NSString *)secret;
|
||||
|
||||
/// 秒级时间戳(字符串)
|
||||
+ (NSString *)currentTimestamp;
|
||||
|
||||
/// 简单 nonce 生成(默认 16 位)
|
||||
+ (NSString *)generateNonceWithLength:(NSUInteger)length;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
92
Shared/KBSignUtils.m
Normal file
92
Shared/KBSignUtils.m
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// KBSignUtils.m
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2025/12/4.
|
||||
//
|
||||
|
||||
#import "KBSignUtils.h"
|
||||
#import <CommonCrypto/CommonCrypto.h>
|
||||
|
||||
@implementation KBSignUtils
|
||||
|
||||
+ (NSString *)urlEncode:(NSString *)value {
|
||||
if (!value) return @"";
|
||||
// 和 Swift 里的 .urlQueryAllowed 类似
|
||||
NSCharacterSet *allowed = [NSCharacterSet URLQueryAllowedCharacterSet];
|
||||
NSString *encoded = [value stringByAddingPercentEncodingWithAllowedCharacters:allowed];
|
||||
return encoded ?: value;
|
||||
}
|
||||
|
||||
+ (NSString *)hmacSHA256:(NSString *)data secret:(NSString *)secret {
|
||||
const char *cKey = [secret cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
const char *cData = [data cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
|
||||
|
||||
CCHmac(kCCHmacAlgSHA256,
|
||||
cKey,
|
||||
strlen(cKey),
|
||||
cData,
|
||||
strlen(cData),
|
||||
cHMAC);
|
||||
|
||||
// 转十六进制小写字符串
|
||||
NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
||||
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
|
||||
[output appendFormat:@"%02x", cHMAC[i]];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
+ (NSString *)signWithParams:(NSDictionary<NSString *, NSString *> *)params
|
||||
secret:(NSString *)secret {
|
||||
|
||||
// 1. 过滤空值 & sign 自身
|
||||
NSMutableDictionary<NSString *, NSString *> *filtered = [NSMutableDictionary dictionary];
|
||||
[params enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
|
||||
if (!obj || obj.length == 0) return;
|
||||
if ([[key lowercaseString] isEqualToString:@"sign"]) return;
|
||||
filtered[key] = obj;
|
||||
}];
|
||||
|
||||
// 2. 按 key 字典序排序
|
||||
NSArray<NSString *> *sortedKeys = [[filtered allKeys] sortedArrayUsingSelector:@selector(compare:)];
|
||||
|
||||
// 3. 拼接 key=value&key2=value2...&secret=xxx
|
||||
NSMutableArray<NSString *> *components = [NSMutableArray array];
|
||||
for (NSString *key in sortedKeys) {
|
||||
NSString *value = filtered[key] ?: @"";
|
||||
NSString *encodedValue = [self urlEncode:value];
|
||||
NSString *part = [NSString stringWithFormat:@"%@=%@", key, encodedValue];
|
||||
[components addObject:part];
|
||||
}
|
||||
|
||||
NSString *encodedSecret = [self urlEncode:secret];
|
||||
NSString *secretPart = [NSString stringWithFormat:@"secret=%@", encodedSecret];
|
||||
[components addObject:secretPart];
|
||||
|
||||
NSString *dataString = [components componentsJoinedByString:@"&"];
|
||||
|
||||
// 4. HMAC-SHA256
|
||||
NSString *sign = [self hmacSHA256:dataString secret:secret];
|
||||
return sign;
|
||||
}
|
||||
|
||||
+ (NSString *)currentTimestamp {
|
||||
NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
|
||||
long long seconds = (long long)ts;
|
||||
return [NSString stringWithFormat:@"%lld", seconds];
|
||||
}
|
||||
|
||||
+ (NSString *)generateNonceWithLength:(NSUInteger)length {
|
||||
// 用 UUID 生成,去掉 - , 再截取前 length 位
|
||||
NSString *uuid = [[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""];
|
||||
if (length == 0 || length > uuid.length) {
|
||||
return uuid;
|
||||
}
|
||||
return [uuid substringToIndex:length];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
44
Shared/KBUserSessionManager.h
Normal file
44
Shared/KBUserSessionManager.h
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// KBUserSessionManager.h
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2025/12/1.
|
||||
//
|
||||
|
||||
// KBUserSessionManager.h
|
||||
// 主 App + 键盘扩展 共用的登录会话管理
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class KBUser;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBUserSessionManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 当前登录用户(可能为 nil)
|
||||
@property (atomic, strong, readonly, nullable) KBUser *currentUser;
|
||||
|
||||
/// 是否已登录:由 KBAuthManager 基于 accessToken 判断
|
||||
@property (nonatomic, assign, readonly) BOOL isLoggedIn;
|
||||
|
||||
/// 启动时调用:做“卸载重装”检测 & 恢复本地缓存
|
||||
- (void)bootstrapIfNeeded;
|
||||
|
||||
/// 当前 accessToken,可能为 nil/空字符串
|
||||
- (nullable NSString *)accessToken;
|
||||
|
||||
/// 登录成功统一入口:写 Keychain + 缓存用户信息
|
||||
- (void)handleLoginSuccessWithUser:(KBUser *)user;
|
||||
|
||||
/// 退出登录:清 Keychain + 本地缓存
|
||||
- (void)logout;
|
||||
|
||||
- (void)goLoginVC;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
171
Shared/KBUserSessionManager.m
Normal file
171
Shared/KBUserSessionManager.m
Normal file
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// KBUserSessionManager.m
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2025/12/1.
|
||||
//
|
||||
|
||||
#import "KBUserSessionManager.h"
|
||||
#import "KBAuthManager.h"
|
||||
#import "KBUser.h"
|
||||
#import "KBConfig.h"
|
||||
#import <MJExtension/MJExtension.h>
|
||||
#import "KBLoginVC.h"
|
||||
/// App Group 里的用户缓存 key
|
||||
static NSString * const kKBSessionUserStoreKey = @"KBSession.currentUser";
|
||||
/// “本次安装是否已初始化”标记 key(用来识别卸载重装)
|
||||
static NSString * const kKBSessionInstallFlagKey = @"KBSession.installInitialized";
|
||||
|
||||
@interface KBUserSessionManager ()
|
||||
@property (nonatomic, strong) NSUserDefaults *defaults;
|
||||
@property (atomic, strong, readwrite, nullable) KBUser *currentUser;
|
||||
@property (nonatomic, assign) BOOL didBootstrap;
|
||||
@end
|
||||
|
||||
@implementation KBUserSessionManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBUserSessionManager *m;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
m = [KBUserSessionManager new];
|
||||
});
|
||||
return m;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
// 优先用 App Group 的 UserDefaults,App 和扩展共用一份
|
||||
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||
_defaults = ud ?: [NSUserDefaults standardUserDefaults];
|
||||
|
||||
// 监听 token 变化(例如别处调用了 signOut)
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(_onAuthChanged:)
|
||||
name:KBAuthChangedNotification
|
||||
object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)bootstrapIfNeeded {
|
||||
if (self.didBootstrap) return;
|
||||
self.didBootstrap = YES;
|
||||
|
||||
// 使用本 App 的 standardUserDefaults 记录“本次安装是否已初始化”。
|
||||
// 这样在卸载重装后,这个标记会被系统清空,从而触发一次性清理旧的 Keychain token。
|
||||
NSUserDefaults *installDefaults = [NSUserDefaults standardUserDefaults];
|
||||
BOOL hasInitializedThisInstall = [installDefaults boolForKey:kKBSessionInstallFlagKey];
|
||||
|
||||
KBAuthManager *auth = [KBAuthManager shared]; // 内部会 reloadFromKeychain
|
||||
|
||||
if (!hasInitializedThisInstall) {
|
||||
// 说明是“卸载重装后的第一次运行”(或者真正第一次安装)
|
||||
[installDefaults setBool:YES forKey:kKBSessionInstallFlagKey];
|
||||
[installDefaults synchronize];
|
||||
|
||||
// 若此时 Keychain 里已有 token,多半是上一次安装遗留的;按你的需求清掉
|
||||
if (auth.current.accessToken.length > 0) {
|
||||
[auth signOut];
|
||||
}
|
||||
|
||||
// 清除旧的用户缓存
|
||||
[self.defaults removeObjectForKey:kKBSessionUserStoreKey];
|
||||
[self.defaults synchronize];
|
||||
self.currentUser = nil;
|
||||
} else {
|
||||
// 正常启动:从本地恢复用户信息
|
||||
[self p_loadUserFromStore];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isLoggedIn {
|
||||
return [[KBAuthManager shared] isLoggedIn];
|
||||
}
|
||||
|
||||
- (NSString *)accessToken {
|
||||
return [KBAuthManager shared].current.accessToken;
|
||||
}
|
||||
|
||||
- (void)handleLoginSuccessWithUser:(KBUser *)user {
|
||||
if (!user) return;
|
||||
|
||||
// 先写 Keychain(统一走 KBAuthManager)
|
||||
NSString *token = user.token;
|
||||
if (token.length > 0) {
|
||||
[[KBAuthManager shared] saveAccessToken:token
|
||||
refreshToken:nil
|
||||
expiryDate:nil
|
||||
userIdentifier:nil];
|
||||
}
|
||||
|
||||
// 再缓存用户信息,供 App/键盘使用
|
||||
self.currentUser = user;
|
||||
[self p_saveUserToStore:user];
|
||||
}
|
||||
|
||||
- (void)logout {
|
||||
// 清 Keychain
|
||||
[[KBAuthManager shared] signOut];
|
||||
|
||||
// 清本地用户缓存
|
||||
self.currentUser = nil;
|
||||
[self.defaults removeObjectForKey:kKBSessionUserStoreKey];
|
||||
[self.defaults synchronize];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)p_loadUserFromStore {
|
||||
id obj = [self.defaults objectForKey:kKBSessionUserStoreKey];
|
||||
if (![obj isKindOfClass:[NSDictionary class]]) {
|
||||
self.currentUser = nil;
|
||||
return;
|
||||
}
|
||||
KBUser *user = [KBUser mj_objectWithKeyValues:(NSDictionary *)obj];
|
||||
self.currentUser = user;
|
||||
}
|
||||
|
||||
- (void)p_saveUserToStore:(KBUser *)user {
|
||||
if (!user) {
|
||||
[self.defaults removeObjectForKey:kKBSessionUserStoreKey];
|
||||
[self.defaults synchronize];
|
||||
return;
|
||||
}
|
||||
NSDictionary *dict = [user mj_keyValues];
|
||||
if (dict) {
|
||||
[self.defaults setObject:dict forKey:kKBSessionUserStoreKey];
|
||||
[self.defaults synchronize];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_onAuthChanged:(NSNotification *)note {
|
||||
// 避免在 KBAuthManager 的 +shared 初始化过程中再次调用 +shared 造成递归,
|
||||
// 这里直接使用通知携带的实例。
|
||||
KBAuthManager *auth = nil;
|
||||
if ([note.object isKindOfClass:[KBAuthManager class]]) {
|
||||
auth = (KBAuthManager *)note.object;
|
||||
} else {
|
||||
auth = [KBAuthManager shared];
|
||||
}
|
||||
|
||||
// 如果 token 被别处清掉了,这里同步清一下 currentUser
|
||||
if (![auth isLoggedIn]) {
|
||||
self.currentUser = nil;
|
||||
[self.defaults removeObjectForKey:kKBSessionUserStoreKey];
|
||||
[self.defaults synchronize];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)goLoginVC{
|
||||
KBLoginVC *vc = [[KBLoginVC alloc] init];
|
||||
[KB_CURRENT_NAV pushViewController:vc animated:true];
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user