1.4.0
升级路径:1.3.3 → 1.4.0 | 本次为双 API 语义统一(破坏性变更)
⚠️ 行为变更(升级必读)
AuthUtil/TinySecurityFacade的get*与has*系列在未登录/会话失效时统一抛UnAuthorizedException,不再返回 null:- 影响方法:
getSecurityContext/getLoginSubject/getLoginId/getLoginIdAsString/getLoginIdAsInt/getLoginIdAsLong/getRoleSet/getPermissionSet/hasRole/hasAllRole/hasAnyRole/hasPermission/hasAllPermission/hasAnyPermission - 此前
AuthProvider系列已抛异常,而AuthUtil/TinySecurityFacade系列返回 null,两套 API 行为不一致;本次统一为"未登录即抛异常",与AuthProvider、Spring Security 语义对齐 - 破坏性:依赖
AuthUtil.getLoginId()等返回 null 进行容错判断(如可选鉴权接口、日志埋点)的代码,升级后将抛UnAuthorizedException,需改为显式捕获或先判断登录状态
- 影响方法:
- 移除冗余转发类
SecurityContextUtils(原仅转发到AuthUtil,无自有逻辑):直接使用AuthUtil.getSecurityContext()等替代。破坏性:若外部代码 import 了SecurityContextUtils,请改用AuthUtil。 - 移除冗余转发类
ThreadLocalSecurityContextHolder(原仅封装一个ThreadLocal):ThreadLocal已内联进ThreadLocalSecurityContextRepository,通过其静态方法getContext()/setContext()/clearContext()访问。破坏性:若外部代码 import 了ThreadLocalSecurityContextHolder,请改用ThreadLocalSecurityContextRepository。 - 移除冗余抽象
LocalMapContainer接口及其唯一实现LocalMapContainerByConcurrentHashMap:LocalTimeCache的dataMap/expireMap字段与构造直接使用ConcurrentHashMap(底层本就依赖它,行为不变,删除getSource()死方法)。破坏性:若外部代码直接 newLocalTimeCache(...),参数类型改为ConcurrentHashMap<String,Object>/ConcurrentHashMap<String,Long>。 SessionRepository.deleteByLoginId返回值语义统一为"操作已执行"(幂等):此前 Single/Caffeine 无会话时返回true,而 Jdbc/Redis 返回false,四个仓储行为不一致。本次统一为:只要操作正常完成即返回true(无会话可删也算成功,仅异常返回false)。破坏性:依赖 Jdbc/RedisdeleteByLoginId返回false判断"该账号无会话"的代码,升级后将收到true,请改用countValidOnlineSessions判断是否存在会话。
升级检查清单
- 排查代码中所有
AuthUtil.getLoginId()/AuthUtil.getLoginSubject()/AuthUtil.getSecurityContext()/AuthUtil.getRoleSet()/AuthUtil.getPermissionSet()及has*系列的调用点 - 若存在"未登录也调用上述方法且依赖返回 null"的场景(如未登录可访问的接口里读取当前用户做日志),改为:先
try { AuthUtil.getLoginId(); } catch (UnAuthorizedException e) { /* 未登录处理 */ },或在确信已登录的路径内调用 -
hasRole/hasPermission等"判断型"方法:未登录时也会抛异常(不再返回 false)。若希望未登录容错判断,需在调用前确保已登录或显式捕获 - 全局异常处理器若已处理
UnAuthorizedException(默认TinySecurityHandlerExceptionResolver会转为 401),则无需额外改动
新增特性
- 新增按账号查询会话凭证
getCredentialsByLoginId(Object loginId):返回该账号下全部**有效(未过期)**会话凭证列表,无会话时返回空列表(不返回 null)。- 用途:多设备管理(列出账号所有在线设备/会话)、定向下线(配合
deleteByCredentials只踢指定凭证,而非deleteByLoginId全量踢出) - 入口:
AuthProvider.getCredentialsByLoginId(loginId);四个内置仓储(single / caffeine / redis / jdbc)均已实现 - 接口方法声明为
default,默认实现抛UnsupportedOperationException:自定义仓储未实现时显式报错而非静默返回空列表,同时保持存量自定义仓储的编译兼容 ⚠️ 安全提示:凭证等同于会话钥匙,请勿写入日志或返回给前端
- 用途:多设备管理(列出账号所有在线设备/会话)、定向下线(配合
- 新增 Caffeine 本地缓存会话仓储(
store-type=caffeine):提供容量上限保护(caffeine-maximum-size,默认 10000,近似 LRU 驱逐)与惰性过期驱逐,适合"单机部署、会话量大、需内存上限"场景。- 可选依赖:caffeine 以
provided提供,使用前需自行引入:<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
- 过期语义与其它仓储一致:由
LoginSubject.getLoginExpireTime()驱动,续期时重新写入即重置。
- 可选依赖:caffeine 以
契约修复
getCredentialsByLoginId返回空列表时统一为可变ArrayList:此前空列表分支返回Collections.emptyList()(不可变),非空分支返回new ArrayList<>(list)(可变),同一份语义却给出不同 mutability(外部add/remove会爆UnsupportedOperationException)。本次将所有内置仓储的空列表分支统一改为new ArrayList<>(),保证契约一致。
测试加固
- 新增
AuthProviderCredentialsTest(4 用例):门面透传、空账号返回空列表、logout()后查询收敛、自定义仓储未实现时抛UnsupportedOperationException - 四个内置仓储测试各 +2 用例:覆盖多凭证返回、过期凭证排除、空账号可变空列表、返回防御性副本
1.3.3
升级路径:1.3.2 → 1.3.3 | 本次为安全加固 + 正确性修复批次(基于 2026-08-31 安全审计)
安全修复
- 恢复
LoginSubject.toString()凭证脱敏:1.3.2 后aae08b7曾回退为输出真实 credentials(便于排查),现恢复为固定输出credentials=****,杜绝日志泄露会话凭证;同步恢复并修正LoginSubjectTest。 - 移除可预测凭证生成器:删除
snowflake/objectid/ulid三种凭证风格(含时间戳,凭证可被预测/枚举,存在会话冒充风险),仅保留uuid/random128/nanoid;CredentialsGenUtil对未知风格一律回退uuid。同时删除仅被其引用的Snowflake/ObjectId/Ulid*/LocalHostUtil实现。 - URL 参数 token 默认关闭:新增
enable-url-token(默认false),token 默认仅从 header(及开启后的 Cookie)读取,不再回退 URL 参数,避免凭证进入访问日志/Referer。
Bug 修复 / 正确性
- 滑动续期阈值修正:
DefaultAuthenticationManager续期阈值由timeout*0.8改为timeout*0.2(剩余 TTL 不足 20% 才续期),消除"会话度过 20% 后每请求触发存储写"的写放大。 - ThreadLocal 异步残留防御:
AuthenticationInterceptor.preHandle进入时先clearContext兜底清理一次,降低 Servlet 异步/异常场景下线程回池残留导致的"用户串号"风险。 JsonUtil序列化失败显式抛错:不再静默返回空串,改为抛TinySecurityException,杜绝"登录成功但立即 401"的隐蔽故障。SingleSessionRepository在线索引并发收敛:list 变更统一由单锁保护,弃用put整体替换,修复并发计数丢失更新。login()成功路径异常隔离:成功事件发布移出 try 块,自定义事件监听器抛异常不再被误判为登录失败(仅 WARN)。getLoginIdAsInt/Long异常语义统一:loginId 非数字时抛TinySecurityException(不再直接抛NumberFormatException导致 500)。
API 调整
JwtUtil新增getVerifiedSubject(secret, token)(验签后取 subject);原getSubject(token)不验签,标注@Deprecated并在 javadoc 中警告;sign增加 payload 空校验。
工程清理
- 清理生产类残留
main()/System.out/printStackTrace(BCrypt/SimpleHash/SM3Hash/SM3ConvertUtil/banner 输出);README 版本号同步至 1.3.3 并更新配置说明。
1.3.2
发布日期:2026-08-27 | 升级路径:1.3.1 → 1.3.2 | 完整分析见
docs/project-analysis-2026-08.md
类型概览
| 维度 | 内容 |
|---|---|
| 新增特性 | 4 项(enable-cookie 开关、jwt-timeout 配置、Cookie 安全属性配置、登出清理浏览器 Cookie) |
| Bug 修复 | 3 项(JWT 公开默认密钥、JWT 过期时间硬编码、非 Web 线程登录 NPE) |
| 安全修复 | 1 项(日志凭证泄露链路) |
| 行为变更 | 2 项(未配置 jwt-secret 时改用随机密钥、Cookie 模式默认关闭) |
安全修复
- 移除 JWT 硬编码默认密钥:此前未配置
tiny-security.jwt-secret时会静默使用一个已在开源文档中公开的内置密钥,且LoginSubject.toString()会输出会话凭证(日志泄露)——两者组合存在伪造会话的风险。现在JwtUtil对空密钥直接抛出IllegalArgumentException,不再有任何内置兜底密钥;LoginSubject.toString()对 credentials 脱敏输出。 - 非 Web 线程登录 NPE 修复:定时任务 / MQ 消费者等未绑定请求上下文的线程调用
login()时,CookieUtil.setCookie不再因response为 null 抛出空指针,改为跳过 Cookie 写入并正常返回 token。
Bug 修复
- JWT 过期时间硬编码 30 天:
timeout配置超过 30 天时会话会在 30 天处静默失效(401)。现新增jwt-timeout配置(默认 2592000 秒即 30 天),且实际生效值取max(jwt-timeout, timeout):默认行为不变,超长会话配置不再提前失效。 - Cookie 生存期与会话不同步:登录 Cookie 的 maxAge 由硬编码 86400 秒改为与会话
timeout一致,不再出现「Cookie 比会话活得久」。 - 登出未清理浏览器 Cookie:
logout()/logout(request)现在会写回maxAge=0的同名 Cookie,浏览器侧凭证立即失效。
新增特性
-
Cookie 模式开关(
enable-cookie):tiny-security: enable-cookie: false # 默认 false;开启后登录写 Cookie、登出清理 Cookie、并允许从 Cookie 读取 token
-
JWT 自身有效期可配置(
jwt-timeout):tiny-security: jwt-timeout: 2592000 # 单位秒,默认30天;实际生效值不低于会话timeout
-
Cookie 安全属性配置:
tiny-security: cookie-secure: false # 是否仅 HTTPS 传输,默认 false(生产建议开启) cookie-same-site: LAX # SameSite 属性,默认 LAX(可选 STRICT/LAX/NONE,防御 CSRF)
-
未配置 jwt-secret 时的安全兜底:启动时自动生成 128 位随机密钥并打印 WARN 日志(提示重启后会话失效、生产环境必须配置固定密钥),替代原先静默使用公开默认密钥的行为。
⚠️ 行为变更(升级必读)
- Cookie 模式默认关闭(
enable-cookie,默认false):此前登录总是写 Cookie、且 token 会自动从 Cookie 中读取;现在默认纯 token 模式(仅 header 与 URL 参数)。前后端不分离、依赖 Cookie 传递 token 的项目升级后必须显式配置enable-cookie: true,否则登录不再写 Cookie、请求也无法从 Cookie 中取到 token(全部 401)。 - 未配置
jwt-secret时改用随机密钥:此前未配置密钥的项目可以跨重启保持会话(因为使用内置固定密钥);升级后每次重启会话全部失效。生产环境请务必配置固定密钥。 - 直接调用
JwtUtil.sign(null, ...)/JwtUtil.getClaims(null, ...)的代码将抛出IllegalArgumentException(原先静默使用内置密钥);JwtUtil.sign新增带expireSeconds参数的重载,原三参重载保持兼容(默认 30 天)。
升级检查清单
- 依赖 Cookie 传 token 的(前后端不分离)项目:务必配置
enable-cookie: true,否则升级后无法从 Cookie 读取 token - 检查是否已配置
tiny-security.jwt-secret(未配置的项目重启后会话失效) - 如直接使用
JwtUtil工具类,确认未依赖「空密钥走内置默认值」的旧行为 - 生产环境建议开启
cookie-secure: true -
LoginSubject.toString()不再输出 credentials,如有依赖该输出的日志排查逻辑需注意