tiny-security 是一款基于 SpringBoot 开发的轻量级 Java Web 权限认证框架,致力于让认证鉴权变得简单高效。 核心特性
- 支持登录认证与权限认证双重保障
- 兼容 token 验证与 cookie 验证两种模式
- 提供多种会话存储方案:redis、jdbc 和单机 session(支持自定义会话存储)
- 无缝适配前后端分离与不分离项目
- 完善的文档,包括使用说明、API文档、最佳实践等
- JDK 8 及以上版本
- SpringBoot 2.x 或 3.x 项目
根据 SpringBoot 版本选择对应的 starter:
SpringBoot 2.x
<dependency>
<groupId>top.lxyccc</groupId>
<artifactId>tiny-security-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>SpringBoot 3.x
<dependency>
<groupId>top.lxyccc</groupId>
<artifactId>tiny-security-boot3-starter</artifactId>
<version>1.3.1</version>
</dependency>tiny-security:
# 存储类型,目前支持jdbc和redis和单机内存三种(redis,jdbc,single),如不配置,则默认为singlestore-type: single# token名称 (同时也是cookie名称以适配前后端不分离的模式)token-name: token# 会话有效期(会话存储中的subject有效时长),单位秒,默认1800秒(30分钟)# 注意:当前版本JWT自身过期时间固定为30天(用于防伪校验),后续版本将支持配置化timeout: 1800# 最大登录并发数,默认不限制max-concurrent-logins: 2# credentials凭证类型,可配置uuid(默认风格),snowflake(纯数字风格),objectid(变种uuid),random128 (随机128位字符串),nanoid,ulidcredentials-style: uuid# 当配置为jdbc时,存储会话信息的表名字,默认为t_auth_storagetable-name: t_auth_storage# 是否开启权限(角色)校验,默认false不开启,开启后需要实现AuthorizationInfoGet接口authorization-enabled: true# 是否启用框架默认异常翻译器(自动将框架异常转换为JSON响应),默认trueexception-translation-enabled: true# 是否强制以 HTTP 200 返回异常响应,默认false;开启后异常统一返回200,由响应体 code 字段表达真实错误force-http-status-200: false# 权限校验方式,可配置ANNOTATION(注解方式)、URL(url方式)perm-check-mode: ANNOTATION# jwt密钥,不配置则使用默认值jwt-secret: K$N)A3*sGGf<wo*22*%&(DF# jwt主题,不配置则使用默认值jwt-subject: tiny-security# 要拦截的路径,默认拦截所有路径add-path: /**# 要排除的路径,默认不排除任何路径exclude-path:
- /auth/login
- /auth/getCode
- /auth/register
- /auth/sendEmail- 当
store-type配置为jdbc时,需要配置数据库连接信息,并导入框架提供的sql脚本到数据库中(目前仅提供了MySQL版本) - 当
store-type配置为redis时,需要配置redis连接信息
- 使用jdbc做会话存储容器
依赖于
jdbcTemplate,须导入依赖spring-boot-starter-jdbc,在yml里进行数据库连接的相应配置并导入框架提供的sql脚本(目前仅提供了MySQL版本)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>- 使用redis做会话存储容器
依赖于
stringRedisTemplate,须导入依赖spring-boot-starter-data-redis,并在yml里进行redis连接的相应配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>如需开启权限(角色)校验,还需要实现
AuthorizationInfoGet接口,提供权限和角色编码数据(框架没有对权限和角色标记码进行缓存,如需缓存请自行处理)
@ComponentpublicclassAuthorizationInfoGetImplimplementsAuthorizationInfoGet {
privatefinalstaticLoggerlogger = LoggerFactory.getLogger(PermissionInfoInterfaceImpl.class);
/** * 返回一个账号所拥有的权限码集合 * @param subject 登录主体,包含loginId、登录凭证等信息 */@OverridepublicSet<String> getPermissionSet(LoginSubjectsubject) {
if (logger.isInfoEnabled()) {
logger.info("AuthorizationInfoGet -- getPermissionSet -- subject = {}", subject);
}
// 自定义权限编码列表获取逻辑,下面的只是示例Set<String> permissionSet = newHashSet<String>() {{
add("user:read");
add("user:write");
}};
returnpermissionSet;
}
/** * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) * @param subject 登录主体,包含loginId、登录凭证等信息 */@OverridepublicSet<String> getRoleSet(LoginSubjectsubject) {
if (logger.isInfoEnabled()) {
logger.info("AuthorizationInfoGet -- getRoleSet -- subject = {}", subject);
}
// 自定义角色编码列表获取逻辑,下面的只是示例Set<String> roleSet = newHashSet<String>() {{
add("admin");
add("user");
}};
returnroleSet;
}
}@RestControllerpublicclassLoginController {
@AutowiredprivateAuthProviderauthProvider;
@PostMapping("/login")
publicResult<Object> login(@RequestParam("username") Stringusername,
@RequestParam("password") Stringpassword) {
// 1. 你的登录验证逻辑,例如:校验用户名密码是否正确if (!verifyUser(username, password)) {
returnResult.fail("用户名或密码错误!");
}
// 2. 签发token(loginId建议使用用户ID或用户名,需保证全局唯一)Stringtoken = authProvider.login(username);
// 或额外携带其他会话信息,例如:用户id、用户名、手机号、邮箱等等// String token = authProvider.login(username, Map.of("userId", entity.getId()));returnResult.ok("登录成功!", token);
}
// 自定义用户校验privatebooleanverifyUser(Stringusername, Stringpassword) {
// 实际项目中对接数据库验证return"admin".equals(username) && "123456".equals(password);
}
}注意:
login返回的token默认已带Bearer前缀(例如Bearer eyJ...),前端传递时应原样携带。 login方法参数说明:
- loginId 登录的账号id,建议的数据类型:long | int | String,建议为用户id,不可以传入复杂类型,如:User、Admin 等等
@ControllerpublicclassIndexController {
@AutowiredprivateAuthProviderauthProvider;
@ResponseBody@GetMapping("/logout")
publicResult<Object> logout(HttpServletRequestrequest) {
// 退出登录,注销会话authProvider.logout(request);
// 不传入request亦可,会自动获取当前的request// authProvider.logout();returnResult.ok("退出登录成功!");
}
}@AutowiredprivateAuthProviderauthProvider;
// 获取登录ID,无会话时会抛出异常ObjectloginId = authProvider.getLoginId();
StringloginIdStr = authProvider.getLoginIdAsString();
LongloginIdLong = authProvider.getLoginIdAsLong();
// 获取登录安全上下文,无会话时会抛出异常LoginSubjectloginSubject = authProvider.getLoginSubject();也可使用静态工具类 AuthUtil:
// (这个方法在无会话时不会抛出异常,而是返回null),还可以直接getLoginIdAsString(), getLoginIdAsInt(), getLoginIdAsLong()ObjectloginId = AuthUtil.getLoginId();
// (这个方法在无会话时不会抛出异常,而是返回null)SecurityContextsecurityContext = AuthUtil.getSecurityContext();
LoginSubjectloginSubject = securityContext == null ? null : securityContext.getLoginSubject();@AutowiredprivateAuthProviderauthProvider;
Stringtoken = authProvider.getToken();
// 或者Stringtoken = authProvider.getToken(HttpServletRequest);@AutowiredprivateAuthProviderauthProvider;
Stringcredentials = authProvider.getCredentials();
// 或者Stringcredentials = authProvider.getCredentials(HttpServletRequest);在Controller的方法或类上面添加@Ignore注解可排除框架会话拦截,即表示调用接口不用传递token了。
@AutowiredprivateAuthProviderauthProvider;
// 根据token,使会话注销authProvider.deleteByToken(token);
// 根据会话凭证credentials,使会话注销authProvider.deleteByCredentials(credentials);
// 根据用户loginId,使该用户的全部会话都注销authProvider.deleteTokenByLoginId(loginId);1.注解解释:
// 需要有 system:user:add 权限才能访问@RequiresPermissions("system:user:add")
// 需要有 system:user:add 和 system:user:delete 权限才能访问, logical可以不写,默认是AND@RequiresPermissions(value={"system:user:add", "system:user:delete"}, logical=Logical.AND)
// 需要有 system:user:add 或 system:user:delete 权限才能访问@RequiresPermissions(value={"system:user:add", "system:user:delete"}, logical=Logical.OR)
// 需要有user角色才能访问@RequiresRoles(value="user")
// 需要有admin和user角色才能访问@RequiresRoles(value={"admin", "user"}, logical=Logical.AND)
// 需要有admin或user角色才能访问@RequiresRoles(value={"admin", "user"}, logical=Logical.OR)注解加在Controller的方法或类上面
2.代码示例:
@ControllerpublicclassIndexController {
finalstaticLoggerlogger = LoggerFactory.getLogger(IndexController.class);
@AutowiredprivateAuthProviderauthProvider;
@RequiresPermissions("权限3")
@ResponseBody@GetMapping("/testPermission")
publicResult<Object> testPermission() {
returnResult.ok("testPermission测试成功!");
}
@RequiresRoles(value="角色1")
@ResponseBody@GetMapping("/testRole")
publicResult<Object> testRole() {
logger.info("SecurityContext = {}", authProvider.getSecurityContext());
logger.info("authProvider.getLoginId() = {}", authProvider.getLoginId());
logger.info("AuthUtil.getLoginId() = {}", AuthUtil.getLoginId());
logger.info("token = {}", authProvider.getToken());
returnResult.ok("testRole测试成功!", authProvider.getLoginId());
}
}1.代码示例:
// 判断:当前账号是否含有指定角色, 返回 true 或 falseAuthUtil.hasRole("role1");
// 判断:当前账号是否含有指定角色 [指定多个,必须全部验证通过]AuthUtil.hasAllRole("role1", "role2");
// 判断:当前账号是否含有指定角色 [指定多个,只要其一验证通过即可]AuthUtil.hasAnyRole("role1", "role2");
// 判断:当前账号是否含有指定权限, 返回 true 或 falseAuthUtil.hasPermission("permission1");
// 判断:当前账号是否含有指定权限 [指定多个,必须全部验证通过]AuthUtil.hasAllPermission("permission1", "permission2");
// 判断:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]AuthUtil.hasAnyPermission("permission1", "permission2");PermissionInfoInterfaceImpl实现类里返回的权限编码要和接口URL相匹配(需要带上context-path)
🚨支持使用通配符指定泛权限,例如当一个账号拥有system:user:*的权限时,system:user:add、system:user:delete、system:user:update都将匹配通过
⚠️ 注意 当一个账号拥有*权限时,可以验证通过任何权限码 (角色认证同理), 所以请谨慎使用*权限码
tiny-security在会话验证失败和权限验证失败的会抛出自定义异常:
| 自定义异常 | 描述 | 错误信息 |
|---|---|---|
| TinySecurityException | 基础异常 | 错误信息“系统异常!”,错误码500 |
| UnAuthorizedException | 未登录或会话已失效 | 错误信息“未登录或会话已失效!”,错误码401 |
| NoPermissionException | 无权限访问(角色或者资源不匹配) | 错误信息“无权限访问!”,错误码403 |
| ConcurrentLoginOverLimitException | 并发登录超过限制 | 错误信息“并发登录超过最大限制!”,错误码409 |
默认情况下,框架已内置异常翻译器,会自动将上述异常转换为JSON响应(可通过 tiny-security.exception-translation-enabled=false 关闭)。
默认情况下,框架会返回与异常对应的真实 HTTP 状态码(401 / 403 / 409 / 500),方便网关、前端按状态码做统一拦截。
如果你的前端或网关希望所有异常都统一返回 HTTP 200,仅通过响应体里的 code 字段区分错误(例如某些前端框架对 4xx/5xx 有额外拦截、或需要与旧系统兼容),可开启以下配置:
tiny-security:
# 是否强制以 HTTP 200 返回异常响应,默认 false# true : 异常统一返回 200,响应体 code 仍为真实业务错误码(401/403/409/500),不影响前端判错# false : 返回真实错误状态码(401/403/409/500)force-http-status-200: true说明:开启
force-http-status-200: true后,HTTP 状态码固定为 200,但响应体中的code字段仍然是真实的业务错误码,前端仍可据此判断具体错误类型,无需改动判错逻辑。
如果你希望完全自定义返回结构,也可以自己编写全局异常处理器来接管返回:
@ControllerAdvicepublicclassGlobalExceptionHandler {
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/** * 统一处理 TinySecurityException 及其子类异常(UnAuthorizedException、NoPermissionException、ConcurrentLoginOverLimitException) * * @param e 父类 TinySecurityException(实际接收子类实例) */@ExceptionHandler(TinySecurityException.class)
publicApiResult<?> handleAuthException(TinySecurityExceptione) {
// 判断具体异常类型if (einstanceofUnAuthorizedException) {
// 未会话异常:使用子类的错误码returnApiResult.fail(e.getCode(), I18nUtils.getMessage(e.getCode()));
} elseif (einstanceofNoPermissionException) {
// 无权限异常:使用子类的错误码returnApiResult.fail(e.getCode(), I18nUtils.getMessage(e.getCode()));
} elseif (einstanceofConcurrentLoginOverLimitException) {
// 并发登录超过最大限制:使用子类的错误码returnApiResult.fail(e.getCode(), I18nUtils.getMessage(e.getCode()));
} else {
// 兜底:处理 TinySecurityException 其他可能的子类(避免漏判)log.warn("未明确处理的 TinySecurityException 子类:{},错误码:{}", e.getClass().getName(), e.getCode());
returnApiResult.fail(e.getCode(), I18nUtils.getMessage(e.getCode()));
}
}
}- 放在参数里面用
token传递:
$.get("/xxx",{"token": token},function(data){});- 放在header里面用
token传递:
$.ajax({url: "/xxx",beforeSend: function(xhr){// token应包含Bearer前缀,例如:Bearer eyJ...xhr.setRequestHeader("token",token);},success: function(data){}});- 前后端不分离的项目会自动从cookie里获取
token
框架内置了 JdbcSessionRepository、RedisSessionRepository 和 SingleSessionRepository 三种会话仓储实现。
如果你想把会话存储到其他介质(例如 MongoDB),可以自定义 SessionRepository:
@Component@ConditionalOnProperty(name = "tiny-security.store-type", havingValue = "mongo")
publicclassMongoSessionRepositoryimplementsSessionRepository {
@Overridepublicbooleansave(LoginSubjectsubject, inttimeoutSeconds, intmaxConcurrentLogins) {
// 保存会话returntrue;
}
@OverridepublicbooleancheckByCredentials(Stringcredentials) {
returnfalse;
}
@OverridepublicLoginSubjectgetSubject(Stringcredentials) {
returnnull;
}
@OverridepublicbooleanrefreshByCredentials(Stringcredentials, LoginSubjectsubject, inttimeoutSeconds) {
returntrue;
}
@OverridepublicbooleandeleteByCredentials(Stringcredentials) {
returntrue;
}
@OverridepublicbooleandeleteByLoginId(ObjectloginId) {
returntrue;
}
@OverridepublicintcountValidOnlineSessions(ObjectloginId) {
return0;
}
}- 配置
tiny-security:
store-type: mongo框架封装了一些常见的密码哈希与摘要算法,可供使用(如需对称/非对称加密,推荐直接使用 JDK JCA 或 Hutool、BouncyCastle 等专用密码学库)
- 摘要算法: 支持MD5、SHA256和国密SM3算法
newMD5Hash("123456", "323@#@$1234da", 1).toHex();
newMD5Hash("123456", "323@#@$1234da").toHex();
newMD5Hash("123456").toHex();
newMD5Hash("123456", "323@#@$1234da", 2).toHex();
newMD5Hash("123456", "323@#@$1234da", 3).toHex();
newMD5Hash("123456", "323@#@$1234da", 3).toBase64();
newSha256Hash("123456", "323@#@$1234da", 10).toBase64();
newSha256Hash("123456", "323@#@$1234da").toHex();
newSha256Hash("123456").toHex();
newSha256Hash("123456", "323@#@$1234da", 2).toHex();
newSha256Hash("123456", "323@#@$1234da", 3).toHex();
newSha256Hash("123456", "323@#@$1234da", 3).toBase64();
newSM3Hash("123456", "323@#@$1234da", 1).toHex();
newSM3Hash("123456", "323@#@$1234da").toHex();
newSM3Hash("123456").toHex();
newSM3Hash("123456", "323@#@$1234da", 2).toHex();
newSM3Hash("123456", "323@#@$1234da", 4).toHex();
newSM3Hash("123456", "323@#@$1234da").toBase64();- 密码哈希算法 支持BCrypt算法
// 密码哈希StringhashedPassword = BCrypt.hashpw("123456", BCrypt.gensalt());
System.out.println(hashedPassword);
// 密码校验booleanisPasswordMatch = BCrypt.checkpw("123456", hashedPassword);
System.out.println(isPasswordMatch);框架内置了安全事件发布器,默认会发布以下事件:
LoginSuccessEvent:登录成功事件LoginFailureEvent:登录失败事件AuthorizationFailureEvent:鉴权失败事件
框架默认使用 Spring 事件总线发布安全事件,你可以直接监听:
@ComponentpublicclassSecurityEventListener {
@EventListenerpublicvoidonLoginSuccess(org.tinycloud.security.event.LoginSuccessEventevent) {
System.out.println("登录成功: " + event.getLoginId());
}
@EventListenerpublicvoidonLoginFailure(org.tinycloud.security.event.LoginFailureEventevent) {
System.out.println("登录失败: " + event.getLoginId() + ", reason=" + event.getErrorMessage());
}
@EventListenerpublicvoidonAuthorizationFailure(org.tinycloud.security.event.AuthorizationFailureEventevent) {
System.out.println("鉴权失败: " + event.getLoginId() + ", path=" + event.getRequestPath());
}
}如果你想把事件发送到消息队列、审计平台或日志系统,可以自定义 SecurityEventPublisher Bean。
当项目里存在自定义 Bean 时,会自动覆盖框架默认实现:
@ConfigurationpublicclassSecurityEventConfig {
@BeanpublicSecurityEventPublishersecurityEventPublisher() {
returnnewSecurityEventPublisher() {
@OverridepublicvoidpublishLoginSuccess(LoginSuccessEventevent) {
// 例如:发送到MQ或写入审计日志System.out.println("[AUDIT] 登录成功: " + event.getLoginId() + ", time=" + event.getTimestamp());
}
@OverridepublicvoidpublishLoginFailure(LoginFailureEventevent) {
System.out.println("[AUDIT] 登录失败: " + event.getLoginId() + ", reason=" + event.getErrorMessage());
}
@OverridepublicvoidpublishAuthorizationFailure(AuthorizationFailureEventevent) {
System.out.println("[AUDIT] 鉴权失败: " + event.getLoginId() + ", path=" + event.getRequestPath());
}
};
}
}提示:你也可以保留默认发布器,再通过
@EventListener监听并转发到外部系统,这样代码更简洁。