diff --git a/src/helpers/SimApiAuth.cj b/src/helpers/SimApiAuth.cj index 6d9e237..fe82d78 100644 --- a/src/helpers/SimApiAuth.cj +++ b/src/helpers/SimApiAuth.cj @@ -8,6 +8,7 @@ package simapi.helpers import std.collection.* import std.collection.concurrent.* import std.convert.* +import std.time.* import stdx.encoding.json.* import soulsoft_serialization.* import redis.client.* @@ -18,9 +19,21 @@ import simapi.exceptions.* /** * 认证助手:基于 Header Token 的登录态管理。 * 支持两种存储模式: - * - Redis 模式:配置了 RedisConfiguration 时使用,支持多实例共享。 - * - InMemory 模式:未配置 Redis 时自动使用,重启后登录态丢失。 + * - Redis 模式:配置了 RedisConfiguration 时使用,支持多实例共享(可带密码/DB 索引)。 + * - InMemory 模式:未配置 Redis 时自动使用,登录态带过期时间,重启后丢失。 */ + +/// InMemory 存储项:登录信息 JSON + 过期时间(epoch 毫秒,0 表示不过期) +private struct TokenEntry { + var json: String + var expireAt: Int64 + + public init(json: String, expireAt: Int64) { + this.json = json + this.expireAt = expireAt + } +} + public class SimApiAuth { private static let tokenCachePrefix = "SimApi:Auth:Token:" private static let tokenSetCachePrefix = "SimApi:Auth:User:" @@ -29,8 +42,8 @@ public class SimApiAuth { private var _redisHost: String = "" private var _redisPort: UInt16 = 6379 - // InMemory 模式:token → 登录信息 JSON - private let _tokenStore = ConcurrentHashMap() + // InMemory 模式:token → 登录信息(含过期时间,epoch 毫秒;0 表示不过期) + private let _tokenStore = ConcurrentHashMap() // InMemory 模式:userId → token 集合 private let _userTokens = ConcurrentHashMap>() @@ -41,10 +54,24 @@ public class SimApiAuth { public init(options: SimApiOptions) { let redisConfiguration = options.redisConfiguration if (!redisConfiguration.isEmpty()) { - let (host, port) = parseRedisConfig(redisConfiguration) + let (host, port, password, db) = parseRedisConfig(redisConfiguration) _redisHost = host _redisPort = port - _redis = Some(RedisClient(host, port)) + let client = if (password.isEmpty()) { + // autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构, + // redis-client 库解析偶发失败),直接用 RESP2 协议 + RedisClient(host, port, autoHello: false) + } else { + RedisClient(host, port, autoHello: false, authPassword: Some(password)) + } + // 指定 DB 索引(redis 客户端无 select 方法,直接执行 SELECT 命令) + if (db > 0) { + try { + client.executeString(["SELECT", db.toString()]) + } catch (_: Exception) { + } + } + _redis = Some(client) } } @@ -66,7 +93,7 @@ public class SimApiAuth { redis.sadd(setKey, [Blob.fromUtf8(newToken)]) redis.expire(setKey, expireSeconds) } else { - _tokenStore[newToken] = json + _tokenStore[newToken] = TokenEntry(json, nowMillis() + expireSeconds * 1000) var tokens = _userTokens.get(loginItem._id) if (tokens == None) { tokens = HashSet() @@ -89,7 +116,12 @@ public class SimApiAuth { if (let Some(redis) <- _redis) { redis.set(tokenKey, Blob.fromUtf8(json)) } else { - _tokenStore[token] = json + // 保留原过期时间(对齐 C# update 不刷新 TTL) + let expireAt = match (_tokenStore.get(token)) { + case Some(entry) => entry.expireAt + case None => 0 + } + _tokenStore[token] = TokenEntry(json, expireAt) } return token } @@ -108,7 +140,17 @@ public class SimApiAuth { case None => None } } else { - json = _tokenStore.get(token) + json = match (_tokenStore.get(token)) { + case Some(entry) => + // InMemory 过期检查:超过 expireAt 则移除并视为无效 + if (entry.expireAt > 0 && entry.expireAt < nowMillis()) { + _tokenStore.remove(token) + None + } else { + Some(entry.json) + } + case None => None + } } return match (json) { case Some(j) => Some(parseLoginItem(j)) @@ -202,12 +244,40 @@ public class SimApiAuth { SimApiUtil.newGuid() } - private static func parseRedisConfig(config: String): (String, UInt16) { - let parts = config.split(":") - if (parts.size == 2) { - return (parts[0], UInt16.parse(parts[1])) + /// 解析 Redis 连接串,支持:host:port | host:port,password=xxx | host:port,password=xxx,db=2 + private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) { + var host = "127.0.0.1" + var port = 6379u16 + var password = "" + var db: Int64 = 0 + let segments = config.split(",") + let hp = segments[0].split(":") + if (hp.size == 2) { + host = hp[0] + port = UInt16.parse(hp[1]) + } else { + host = config } - return (config, 6379u16) + for (i in 1..segments.size) { + let seg = segments[i].trimAscii() + match (seg.indexOf("=")) { + case Some(idx) => + let key = seg[0..idx].trimAscii().toAsciiLower() + let value = seg[idx + 1..].trimAscii() + match (key) { + case "password" | "pwd" => password = value + case "db" | "database" | "defaultdatabase" => db = Int64.parse(value) + case _ => () + } + case None => () + } + } + (host, port, password, db) + } + + /// 当前时间(epoch 毫秒) + private static func nowMillis(): Int64 { + DateTime.nowUTC().toUnixTimeStamp().toMilliseconds() } private static func loginItemJson(item: SimApiLoginItem): String { diff --git a/src/helpers/SimApiCache.cj b/src/helpers/SimApiCache.cj index 856d9e7..58d9eea 100644 --- a/src/helpers/SimApiCache.cj +++ b/src/helpers/SimApiCache.cj @@ -8,19 +8,32 @@ package simapi.helpers import std.collection.* import std.collection.concurrent.* import std.convert.* +import std.time.* import redis.client.* import simapi.configurations.* import simapi.exceptions.* +/// InMemory 存储项:缓存值 + 过期时间(epoch 毫秒,0 表示不过期) +private struct CacheEntry { + var value: String + var expireAt: Int64 + + public init(value: String, expireAt: Int64) { + this.value = value + this.expireAt = expireAt + } +} + /** * 缓存助手:Key 自动加前缀 "SimApi:Cache:"。 - * 存储后端与 SimApiAuth 一致:配置了 Redis 用 Redis,否则 InMemory。 + * 存储后端与 SimApiAuth 一致:配置了 Redis 用 Redis(可带密码/DB 索引),否则 InMemory。 + * InMemory 模式同样支持过期(对齐 C# DistributedCache 的过期语义)。 */ public class SimApiCache { private static let prefix = "SimApi:Cache:" private var _redis: ?RedisClient = None - private let _store = ConcurrentHashMap() + private let _store = ConcurrentHashMap() /** * 创建缓存(依赖注入 SimApiOptions)。 @@ -29,8 +42,21 @@ public class SimApiCache { public init(options: SimApiOptions) { let redisConfiguration = options.redisConfiguration if (!redisConfiguration.isEmpty()) { - let (host, port) = parseRedisConfig(redisConfiguration) - _redis = Some(RedisClient(host, port)) + let (host, port, password, db) = parseRedisConfig(redisConfiguration) + let client = if (password.isEmpty()) { + // autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构, + // redis-client 库解析偶发失败),直接用 RESP2 协议 + RedisClient(host, port, autoHello: false) + } else { + RedisClient(host, port, autoHello: false, authPassword: Some(password)) + } + if (db > 0) { + try { + client.executeString(["SELECT", db.toString()]) + } catch (_: Exception) { + } + } + _redis = Some(client) } } @@ -38,7 +64,7 @@ public class SimApiCache { * 设置缓存。 * @param key 缓存键。 * @param value 缓存值(不能为 null)。 - * @param expireSeconds 过期秒数(可选)。 + * @param expireSeconds 过期秒数(可选,<=0 表示不过期)。 */ public func set(key: String, value: String, expireSeconds!: Int64 = -1): Unit { if (let Some(redis) <- _redis) { @@ -48,7 +74,8 @@ public class SimApiCache { redis.set("${prefix}${key}", Blob.fromUtf8(value)) } } else { - _store["${prefix}${key}"] = value + let expireAt = if (expireSeconds > 0) { nowMillis() + expireSeconds * 1000 } else { 0 } + _store["${prefix}${key}"] = CacheEntry(value, expireAt) } } @@ -80,14 +107,51 @@ public class SimApiCache { case None => None } } - return _store.get("${prefix}${key}") + return match (_store.get("${prefix}${key}")) { + case Some(entry) => + if (entry.expireAt > 0 && entry.expireAt < nowMillis()) { + _store.remove("${prefix}${key}") + None + } else { + Some(entry.value) + } + case None => None + } } - private static func parseRedisConfig(config: String): (String, UInt16) { - let parts = config.split(":") - if (parts.size == 2) { - return (parts[0], UInt16.parse(parts[1])) + /// 解析 Redis 连接串,支持:host:port | host:port,password=xxx | host:port,password=xxx,db=2 + private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) { + var host = "127.0.0.1" + var port = 6379u16 + var password = "" + var db: Int64 = 0 + let segments = config.split(",") + let hp = segments[0].split(":") + if (hp.size == 2) { + host = hp[0] + port = UInt16.parse(hp[1]) + } else { + host = config } - return (config, 6379u16) + for (i in 1..segments.size) { + let seg = segments[i].trimAscii() + match (seg.indexOf("=")) { + case Some(idx) => + let key = seg[0..idx].trimAscii().toAsciiLower() + let value = seg[idx + 1..].trimAscii() + match (key) { + case "password" | "pwd" => password = value + case "db" | "database" | "defaultdatabase" => db = Int64.parse(value) + case _ => () + } + case None => () + } + } + (host, port, password, db) + } + + /// 当前时间(epoch 毫秒) + private static func nowMillis(): Int64 { + DateTime.nowUTC().toUnixTimeStamp().toMilliseconds() } } diff --git a/src/helpers/SimApiUtil.cj b/src/helpers/SimApiUtil.cj index 69d88d7..a444888 100644 --- a/src/helpers/SimApiUtil.cj +++ b/src/helpers/SimApiUtil.cj @@ -11,7 +11,9 @@ import std.random.* import stdx.crypto.digest.* import stdx.encoding.hex.* import stdx.encoding.base64.* +import stdx.encoding.json.* import std.regex.* +import soulsoft_serialization.* import simapi.communications.* import simapi.macros.* @@ -153,4 +155,38 @@ public class SimApiUtil { public static func json(obj: ?Any): String { SimApiJson.json(obj) } + + /** + * 从 JSON 字符串反序列化为 T(对齐 C# SimApiUtil.FromJson)。 + * @param T 目标类型(需实现 ISerialization,如 @Serialization DTO、基础类型等)。 + * @param jsonString JSON 字符串。 + * @return 反序列化结果。 + */ + public static func fromJson(jsonString: String): T where T <: ISerialization { + JsonSerializer.deserializeObject(jsonString) + } + + /** + * 对象 Base64 编码(对象 → JSON → Base64,对齐 C# Base64Encode(object))。 + * @param obj 任意对象(DTO/基础类型/HashMap 等)。 + * @return Base64 字符串。 + */ + public static func base64Encode(obj: Any): String { + let json = if (let ser: ISerializable <- obj) { + ser.serializeObject().toJson().toString() + } else { + SimApiJson.json(Some(obj)) + } + base64Encode(json) + } + + /** + * Base64 → JSON → T 反序列化(对齐 C# Base64Decode)。 + * @param T 目标类型(需实现 ISerialization)。 + * @param base64Str Base64 字符串。 + * @return 反序列化结果。 + */ + public static func base64DecodeTo(base64Str: String): T where T <: ISerialization { + fromJson(base64Decode(base64Str)) + } }