Releases: llllllxy/tiny-security
Release list
1.4.0版本发布
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,如有依赖该输出的日志排查逻辑需注意
1.3.1版本发布
tiny-security 1.3.1 升级日志
发布日期:2026-08-09
升级路径:1.3.0 → 1.3.1
适用分支:springboot3(Spring Boot 3.x / JDK 17+) 和 springboot2(Spring Boot 2.x / JDK 8)
一、版本概览
1.3.1 是一个架构重构 + 性能优化 + 安全加固版本。本次更新移除了全局静态单例模式,改为 Spring 原生依赖注入;修复了会话调度线程泄露问题;优化了授权检查的 Redis 访问次数;新增了异常响应状态码控制配置项;同时移除了存在安全隐患的加密工具类。
| 维度 | 说明 |
|---|---|
| 新增特性 | 2 项(异常 HTTP 状态码强制 200、授权按需查询优化) |
| Bug 修复 | 1 项(会话调度线程泄露) |
| 破坏性变更 | 3 项(GlobalConfig 移除、AuthProperties 迁移、AESUtil/RSAUtil 删除) |
| 涉及提交 | 6 次功能性提交 + 1 次版本发布 |
| 代码变更 | +1,288 / -840 行(含测试) |
二、新增特性
2.1 异常响应 HTTP 状态码强制 200(force-http-status-200)
提交: 8199d0c
背景: 部分前端框架(如 axios)对非 2xx 状态码会触发异常拦截,导致业务错误码需要额外处理。新增此配置后,异常响应统一返回 HTTP 200,由响应体 code 字段表达真实业务错误。
配置方式:
tiny-security:
force-http-status-200: true # 默认 false行为说明:
| 配置值 | HTTP 状态码 | 响应体 code 字段 |
|---|---|---|
false(默认) |
真实错误码(401/403/409/500) | 真实业务错误码 |
true |
统一 200 | 真实业务错误码(401/403/409/500) |
兼容性: 前端判错逻辑无需改动——无论开关如何,响应体 code 字段始终保留真实业务错误码。DefaultExceptionTranslator 保留了无参构造函数,向后兼容。
2.2 授权检查按注解类型按需调用 SPI
提交: d4e9e67
背景: 1.3.0 中,一个接口即使只标注了 @RequiresRoles,授权管理器仍会同时查询角色和权限(两次 Redis 访问);反之亦然。
优化内容: DefaultAuthorizationManager 现根据注解类型按需调用 SPI:
| 接口注解 | 1.3.0 行为 | 1.3.1 行为 |
|---|---|---|
仅 @RequiresRoles |
查角色 + 查权限 | 仅查角色 |
仅 @RequiresPermissions |
查角色 + 查权限 | 仅查权限 |
| 同时标注两者 | 查角色 + 查权限 | 查角色 + 查权限 |
PermissionMode.URL |
查角色 + 查权限 | 查角色 + 查权限(URL 模式始终查权限) |
收益: 单注解场景下 Redis 访问次数减半。
三、Bug 修复
3.1 会话调度线程泄露修复
提交: 68e6c7b、3756b8e(补充单测)
问题: JDBC 会话仓储(JdbcSessionRepository)和本地缓存(LocalTimeCache)各自创建了 ScheduledExecutorService 调度线程,但在应用停机时没有真正 shutdown。LocalTimeCache.endRefreshThread() 仅将 refreshFlag 标记设为 false,线程本身仍在运行。
影响场景:
- 应用正常停机时线程未关闭
- Spring DevTools 热重启导致线程累积
- 多上下文(如 Spring MVC + WebSocket)场景下线程泄露
修复方案:
| 类 | 修复方式 |
|---|---|
JdbcSessionRepository |
实现 DisposableBean,destroy() 中调用 executorService.shutdownNow() |
SingleSessionRepository |
实现 DisposableBean,destroy() 中调用 timedCache.endRefreshThread() |
LocalTimeCache |
移除 refreshFlag 标记机制,endRefreshThread() 改为直接 executorService.shutdownNow() |
四、破坏性变更(⚠️ 升级必读)
4.1 移除 GlobalConfig / GlobalConfigUtils 全局静态单例
提交: 776dac9
变更说明:
| 项目 | 1.3.0 | 1.3.1 |
|---|---|---|
| 配置持有方式 | GlobalConfig 全局静态单例 + GlobalConfigUtils 访问器 |
Spring 容器依赖注入 |
| 初始化时机 | ContextRefreshedEvent 事件监听 |
Bean 构造时注入 |
AuthAutoConfiguration |
实现 ApplicationListener<ContextRefreshedEvent> |
移除该接口,改为构造注入 |
删除的类:
org.tinycloud.security.config.GlobalConfig(217 行)org.tinycloud.security.config.GlobalConfigUtils(66 行)
升级影响:
- 如果你的代码中直接调用了
GlobalConfig.getXxx()或GlobalConfigUtils.getGlobalConfig(),需要改为通过 Spring 注入获取对应 Bean。 - 框架内部所有拦截器、
AuthProvider已改为构造注入,框架自身使用不受影响。
4.2 AuthProperties 包路径迁移
提交: 776dac9
| 项目 | 1.3.0 | 1.3.1 |
|---|---|---|
| 所在模块 | tiny-security-boot-starter |
tiny-security-core |
| 完整路径 | org.tinycloud.security.AuthProperties |
org.tinycloud.security.config.AuthProperties |
升级影响:
- 如果你通过
import引用了AuthProperties,需要更新 import 路径。 - yml 配置前缀
tiny-security不变,无需修改配置文件。 @EnableConfigurationProperties(AuthProperties.class)注解由AuthAutoConfiguration自动处理,用户通常无感知。
4.3 移除 AESUtil 和 RSAUtil 加密工具类
提交: 7cb7eb3
删除的类:
org.tinycloud.security.util.secure.AESUtil(196 行)org.tinycloud.security.util.secure.RSAUtil(128 行)
删除原因:
- 对称/非对称加密不属于认证框架职责边界
AESUtil存在硬编码默认密钥安全隐患RSAUtil未使用 OAEP 填充,存在安全风险- 框架核心代码从未引用这两个类
保留的算法:
| 类别 | 算法 | 类名 | 状态 |
|---|---|---|---|
| 摘要算法 | MD5 / SHA-256 / SM3 | DigestUtil / SM3Hash |
✅ 保留 |
| 密码哈希 | BCrypt | BCrypt |
✅ 保留 |
升级影响: 如果你的业务代码使用了 AESUtil 或 RSAUtil,请迁移至以下替代方案:
- JDK JCA(
javax.crypto.*) - Hutool(
cn.hutool.crypto.*) - BouncyCastle
五、架构改进
5.1 引入 TinySecurityFacade 门面模式
提交: 776dac9
新增 org.tinycloud.security.TinySecurityFacade(180 行),设计参照 Sa-Token 的 StpUtil/StpLogic 模式:
AuthUtil(静态外观,用户调用不变)
↓ delegate
TinySecurityFacade(Spring Bean,构造注入依赖)
↓
SecurityContextRepository / AuthorizationEvaluator
设计要点:
AuthUtil是静态外观类,用户侧调用方式(AuthUtil.getLoginId()等)完全不变TinySecurityFacade是 Spring Bean,通过构造注入获取SecurityContextRepository- 内部依赖可测试、可替换,解决了全局静态单例不可 Mock 的问题
5.2 拦截器改为构造注入
提交: 776dac9
| 拦截器 | 1.3.0 依赖获取方式 | 1.3.1 依赖获取方式 |
|---|---|---|
AuthenticationInterceptor |
从 GlobalConfig 静态获取 |
构造注入 AuthenticationManager + SecurityContextRepository |
AuthorizationInterceptor |
从 GlobalConfig 静态获取 |
构造注入 AuthorizationManager + SecurityContextRepository + SecurityEventPublisher + AuthProperties |
六、升级指南
6.1 Maven 依赖升级
<!-- Spring Boot 3.x -->
<dependency>
<groupId>top.lxyccc</groupId>
<artifactId>tiny-security-boot3-starter</artifactId>
<version>1.3.1</version>
</dependency>6.2 升级检查清单
按以下顺序检查你的项目:
- 步骤 1: 全局搜索
GlobalConfig和GlobalConfigUtils,如有引用需改为 Spring 注入方式 - 步骤 2: 全局搜索
org.tinycloud.security.AuthProperties的 import,更新为org.tinycloud.security.config.AuthProperties - 步骤 3: 全局搜索
AESUtil和RSAUtil,如有使用迁移至 JCA / Hutool / BouncyCastle - 步骤 4: 如果自定义了
DefaultExceptionTranslator,确认是否需要传入forceHttpStatus200参数 - 步骤 5: 检查是否有代码直接依赖
LocalTimeCache.refreshFlag字段(已移除) - 步骤 6: 验证
AuthUtil调用方式未变(正常情况下无需改动)
6.3 兼容性说明
| 用户侧 API | 兼容性 |
|---|---|
AuthUtil.getLoginId() 等静态方法 |
✅ 完全兼容,调用方式不变 |
yml 配置 tiny-security.* |
✅ 完全兼容,前缀不变 |
@RequiresRoles / @RequiresPermissions |
✅ 完全兼容 |
自定义 SessionRepository |
✅ 兼容(建议实现 DisposableBean 管理线程) |
自定义 ExceptionTranslator |
✅ 兼容(DefaultExceptionTranslator 保留无参构造) |
直接使用 GlobalConfig |
❌ 已移除,需改为注入 |
直接使用 AESUtil / RSAUtil |
❌ 已移除,需迁移 |
七、提交记录
| 序号 | 提交哈希 | 日期 | 类型 | 说明 |
|---|---|---|---|---|
| 1 | d4e9e67 |
2026-07-18 | feat | 授权检查按注解类型按需调用 SPI,减少 Redis 访问 |
| 2 | 68e6c7b |
2026-07-18 | fix | 会话调度线程泄露修复(JdbcSessionRepository / SingleSessionRepository / LocalTimeCache) |
| 3 | 3756b8e |
2026-07-18 | test | 补充 SessionRepositoryLifecycleTest 生命周期单测 |
| 4 | 8199d0c |
2026-08-07 | feat | 新增 force-http-status-200 异常响应状态码配置项 |
| 5 | 7cb7eb3 |
2026-08-08 | refactor | 移除 AESUtil 和 RSAUtil 加密工具类 |
| 6 | 776dac9 |
2026-08-08 | refactor | 重构安全框架配置和依赖注入机制(移除 GlobalConfig,引入 TinySecurityFacade) |
| 7 | e54d4e8 |
2026-08-09 | release | 版本号升级至 1.3.1 |
八、致谢
感谢所有贡献者和使用者的反馈与支持。如遇升级问题,请提交 Issue。
1.3.0版本发布
1.2.7版本发布
1.2.4版本发布
1.2.3版本发布
1.2.2版本发布
1.1.0 版本更新
1、功能重构,减少刷新次数,大幅度提升效率和性能
2、session表结构重构,提高JdbcAuthProvider的整体性能