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.2.7</version>
</dependency>SpringBoot 3.x
<dependency>
<groupId>top.lxyccc</groupId>
<artifactId>tiny-security-boot3-starter</artifactId>
<version>1.2.7</version>
</dependency>tiny-security:
# 存储类型,目前支持jdbc和redis和单机内存三种(redis,jdbc,single),如不配置,则默认为single
store-type: single
# token名称 (同时也是cookie名称以适配前后端不分离的模式)
token-name: token
# token有效期 (即会话时长),单位秒 默认1800秒(30分钟)
timeout: 1800
# 最大登录并发数,默认不限制
max-concurrent-logins: 2
# credentials凭证类型,可配置uuid(默认风格),snowflake(纯数字风格),objectid(变种uuid),random128 (随机128位字符串),nanoid,ulid
credentials-style: uuid
# 当配置为jdbc时,存储会话信息的表名字,默认为t_auth_storage
table-name: t_auth_storage
# 是否开启权限(角色)校验,默认false不开启,开启后需要实现AuthorizationInfoGet接口
authorization-enabled: true
# 权限校验方式,可配置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接口,提供权限和角色编码数据(框架没有对权限和角色标记码进行缓存,如需缓存请自行处理)
@Component
public class AuthorizationInfoGetImpl implements AuthorizationInfoGet {
private final static Logger logger = LoggerFactory.getLogger(PermissionInfoInterfaceImpl.class);
/**
* 返回一个账号所拥有的权限码集合
* @param subject 登录主体,包含loginId、登录凭证等信息
*/
@Override
public Set<String> getPermissionSet(LoginSubject subject) {
if (logger.isInfoEnabled()) {
logger.info("AuthorizationInfoGet -- getPermissionSet -- subject = {}", subject);
}
// 自定义权限编码列表获取逻辑,下面的只是示例
Set<String> permissionSet = new HashSet<String>() {{
add("user:read");
add("user:write");
}};
return permissionSet;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
* @param subject 登录主体,包含loginId、登录凭证等信息
*/
@Override
public Set<String> getRoleSet(LoginSubject subject) {
if (logger.isInfoEnabled()) {
logger.info("AuthorizationInfoGet -- getRoleSet -- subject = {}", subject);
}
// 自定义角色编码列表获取逻辑,下面的只是示例
Set<String> roleSet = new HashSet<String>() {{
add("admin");
add("user");
}};
return roleSet;
}
}@RestController
public class LoginController {
@Autowired
private AuthProvider authProvider;
@PostMapping("/login")
public Result<Object> login(@RequestParam("username") String username,
@RequestParam("password") String password) {
// 1. 你的登录验证逻辑,例如:校验用户名密码是否正确
if (!verifyUser(username, password)) {
return Result.fail("用户名或密码错误!");
}
// 2. 签发token(loginId建议使用用户ID或用户名,需保证全局唯一)
String token = authProvider.login(username);
// 或额外携带其他会话信息,例如:用户id、用户名、手机号、邮箱等等
// String token = authProvider.login(username, Map.of("userId", entity.getId()));
return Result.ok("登录成功!", token);
}
// 自定义用户校验
private boolean verifyUser(String username, String password) {
// 实际项目中对接数据库验证
return "admin".equals(username) && "123456".equals(password);
}
}login方法参数说明:
- loginId 登录的账号id,建议的数据类型:long | int | String,建议为用户id,不可以传入复杂类型,如:User、Admin 等等
@Controller
public class IndexController {
@Autowired
private AuthProvider authProvider;
@ResponseBody
@GetMapping("/logout")
public Result<Object> logout(HttpServletRequest request) {
// 退出登录,注销会话
authProvider.logout(request);
// 不传入request亦可,会自动获取当前的request
// authProvider.logout();
return Result.ok("退出登录成功!");
}
}@Autowired
private AuthProvider authProvider;
// 获取登录ID,无会话时会抛出异常
Object loginId = authProvider.getLoginId();
String loginIdStr = authProvider.getLoginIdAsString();
Long loginIdLong = authProvider.getLoginIdAsLong();
// 获取登录主体信息,无会话时会抛出异常
LoginSubject LoginSubject = authProvider.getLoginSubject();也可使用静态工具类 AuthUtil:
// (这个方法在无会话时不会抛出异常,而是返回null),还可以直接getLoginIdAsString(), getLoginIdAsInt(), getLoginIdAsLong()
Object loginId = AuthUtil.getLoginId();
// (这个方法在无会话时不会抛出异常,而是返回null)
LoginSubject LoginSubject = AuthUtil.getLoginSubject();@Autowired
private AuthProvider authProvider;
String token = authProvider.getToken();
// 或者
String token = authProvider.getToken(HttpServletRequest);@Autowired
private AuthProvider authProvider;
String credentials = authProvider.getCredentials();
// 或者
String credentials = authProvider.getCredentials(HttpServletRequest);在Controller的方法或类上面添加@Ignore注解可排除框架会话拦截,即表示调用接口不用传递token了。
@Autowired
private AuthProvider authProvider;
// 根据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.代码示例:
@Controller
public class IndexController {
final static Logger logger = LoggerFactory.getLogger(IndexController.class);
@Autowired
private AuthProvider authProvider;
@RequiresPermissions("权限3")
@ResponseBody
@GetMapping("/testPermission")
public Result<Object> testPermission() {
return Result.ok("testPermission测试成功!");
}
@RequiresRoles(value="角色1")
@ResponseBody
@GetMapping("/testRole")
public Result<Object> testRole() {
logger.info("LoginSubject = {}", authProvider.getLoginSubject());
logger.info("authProvider.getLoginId() = {}", authProvider.getLoginId());
logger.info("AuthUtil.getLoginId() = {}", AuthUtil.getLoginId());
logger.info("token = {}", authProvider.getToken());
return Result.ok("testRole测试成功!", authProvider.getLoginId());
}
}1.代码示例:
// 判断:当前账号是否含有指定角色, 返回 true 或 false
AuthUtil.hasRole("role1");
// 判断:当前账号是否含有指定角色 [指定多个,必须全部验证通过]
AuthUtil.hasAllRole("role1", "role2");
// 判断:当前账号是否含有指定角色 [指定多个,只要其一验证通过即可]
AuthUtil.hasAnyRole("role1", "role2");
// 判断:当前账号是否含有指定权限, 返回 true 或 false
AuthUtil.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数据(或者页面):
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 统一处理 TinySecurityException 及其子类异常(UnAuthorizedException、NoPermissionException、ConcurrentLoginOverLimitException)
*
* @param e 父类 TinySecurityException(实际接收子类实例)
*/
@ExceptionHandler(TinySecurityException.class)
public ApiResult<?> handleAuthException(TinySecurityException e) {
// 判断具体异常类型
if (e instanceof UnAuthorizedException) {
// 未会话异常:使用子类的错误码
return ApiResult.fail(e.getCode(), I18nUtils.getMessage(e.getCode()));
} else if (e instanceof NoPermissionException) {
// 无权限异常:使用子类的错误码
return ApiResult.fail(e.getCode(), I18nUtils.getMessage(e.getCode()));
} else if (e instanceof ConcurrentLoginOverLimitException) {
// 并发登录超过最大限制:使用子类的错误码
return ApiResult.fail(e.getCode(), I18nUtils.getMessage(e.getCode()));
} else {
// 兜底:处理 TinySecurityException 其他可能的子类(避免漏判)
log.warn("未明确处理的 TinySecurityException 子类:{},错误码:{}", e.getClass().getName(), e.getCode());
return ApiResult.fail(e.getCode(), I18nUtils.getMessage(e.getCode()));
}
}
}- 放在参数里面用
token传递:
$.get("/xxx", { "token": token }, function(data) {
});- 放在header里面用
token传递:
$.ajax({
url: "/xxx",
beforeSend: function(xhr) {
xhr.setRequestHeader("token", token);
},
success: function(data){ }
});- 前后端不分离的项目会自动从cookie里获取
token
框架内置了JdbcAuthProvider、RedisAuthProvider和SingleAuthProvider三种会话实现, 如果仍然无法满足你的需求,或者你想存在其他什么地方,比如存在磁盘文件、MongoDB中,只需以下三步即可:
- 继承org.tinycloud.security.provider.AbstractAuthProvider抽象类, 实现里面的抽象方法,
- 注入bean,如下
@Component
@ConditionalOnProperty(name = "tiny-security.store-type", havingValue = "mongo")
public class MongoAuthProvider extends AbstractAuthProvider {
// ...
}- 配置
tiny-security:
store-type: mongo框架封装了一些常见的加密算法,可供使用
- 摘要算法: 支持MD5、SHA256和国密SM3算法
new MD5Hash("123456", "323@#@$1234da", 1).toHex();
new MD5Hash("123456", "323@#@$1234da").toHex();
new MD5Hash("123456").toHex();
new MD5Hash("123456", "323@#@$1234da", 2).toHex();
new MD5Hash("123456", "323@#@$1234da", 3).toHex();
new MD5Hash("123456", "323@#@$1234da", 3).toBase64();
new Sha256Hash("123456", "323@#@$1234da", 10).toBase64();
new Sha256Hash("123456", "323@#@$1234da").toHex();
new Sha256Hash("123456").toHex();
new Sha256Hash("123456", "323@#@$1234da", 2).toHex();
new Sha256Hash("123456", "323@#@$1234da", 3).toHex();
new Sha256Hash("123456", "323@#@$1234da", 3).toBase64();
new SM3Hash("123456", "323@#@$1234da", 1).toHex();
new SM3Hash("123456", "323@#@$1234da").toHex();
new SM3Hash("123456").toHex();
new SM3Hash("123456", "323@#@$1234da", 2).toHex();
new SM3Hash("123456", "323@#@$1234da", 4).toHex();
new SM3Hash("123456", "323@#@$1234da").toBase64();- 对称加密 支持AES256-CBC算法
// 原文:
String message = "Helloworld!";
System.out.println("Message: " + message);
// 使用方法(密钥长度需要为32字节,iv长度需要为16字节)
AESUtil aesUtils = AESUtil.builder().secretKey("1G78Av#yej%WZJ3uiSZRz9oy%UAv4AAA").ivParameter("E%BAAAUTvXfwSuGQ").build();
// 加密:
String encrypted = aesUtils.encrypt(message);
System.out.println("加密: " + encrypted);
// 解密:
String decrypted = aesUtils.decrypt(encrypted);
System.out.println("解密: " + decrypted);- 非对称加密 支持RSA2048加密
Map<String, String> pair = generateKeyPair();
String publicKey = pair.get("publicKey");
String privateKey = pair.get("privateKey");
// 使用公钥加密
String encryptedValue = encryptByPublicKey(publicKey, "abcdefg");
System.out.println(encryptedValue);
// 使用私钥解密
String decryptedValue = decryptByPrivateKey(privateKey, encryptedValue);
System.out.println(decryptedValue);- 密码哈希算法 支持BCrypt算法
// 密码哈希
String hashedPassword = BCrypt.hashpw("123456", BCrypt.gensalt());
System.out.println(hashedPassword);
// 密码校验
boolean isPasswordMatch = BCrypt.checkpw("123456", hashedPassword);
System.out.println(isPasswordMatch);