# tiny-security **Repository Path**: leisureLXY/tiny-security ## Basic Information - **Project Name**: tiny-security - **Description**: 一个基于token验证的Java Web权限控制框架,支持redis、jdbc和单机session多种存储方式,前后端分离项目、不分离项目均可使用,功能完善、使用简单、文档清晰,易于扩展。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 5 - **Forks**: 1 - **Created**: 2023-01-10 - **Last Updated**: 2026-01-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: 登录认证, 会话控制, Java, SpringBoot, Security ## README

tiny-security

star

# 1、简介 tiny-security 是一款基于 SpringBoot 开发的轻量级 Java Web 权限认证框架,致力于让认证鉴权变得简单高效。 核心特性 - 支持登录认证与权限认证双重保障 - 兼容 token 验证与 cookie 验证两种模式 - 提供多种会话存储方案:redis、jdbc 和单机 session(支持自定义会话存储) - 无缝适配前后端分离与不分离项目 - 完善的文档,包括使用说明、API文档、最佳实践等 --- # 2、快速入门 ## 2.1、SpringBoot集成 ### 2.1.1 环境准备 - JDK 8 及以上版本 - SpringBoot 2.x 或 3.x 项目 ### 2.1.2 引入依赖 根据 SpringBoot 版本选择对应的 starter: **SpringBoot 2.x** ```xml top.lxyccc tiny-security-boot-starter 1.2.7 ``` **SpringBoot 3.x** ```xml top.lxyccc tiny-security-boot3-starter 1.2.7 ``` ### 2.1.3 配置参数 ```yaml 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 依赖于`jdbcTemplate`,须导入依赖 `spring-boot-starter-jdbc`,在yml里进行数据库连接的相应配置并导入框架提供的sql脚本(目前仅提供了MySQL版本) ```xml org.springframework.boot spring-boot-starter-jdbc ``` 2. **使用redis做会话存储容器** > 依赖于`stringRedisTemplate`,须导入依赖 `spring-boot-starter-data-redis` ,并在yml里进行redis连接的相应配置 ```xml org.springframework.boot spring-boot-starter-data-redis ``` ### 2.1.5 实现AuthorizationInfoGet接口 > 如需开启权限(角色)校验,还需要实现`AuthorizationInfoGet`接口,提供权限和角色编码数据(框架没有对权限和角色标记码进行缓存,如需缓存请自行处理) ```java @Component public class AuthorizationInfoGetImpl implements AuthorizationInfoGet { private final static Logger logger = LoggerFactory.getLogger(PermissionInfoInterfaceImpl.class); /** * 返回一个账号所拥有的权限码集合 * @param subject 登录主体,包含loginId、登录凭证等信息 */ @Override public Set getPermissionSet(LoginSubject subject) { if (logger.isInfoEnabled()) { logger.info("AuthorizationInfoGet -- getPermissionSet -- subject = {}", subject); } // 自定义权限编码列表获取逻辑,下面的只是示例 Set permissionSet = new HashSet() {{ add("user:read"); add("user:write"); }}; return permissionSet; } /** * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) * @param subject 登录主体,包含loginId、登录凭证等信息 */ @Override public Set getRoleSet(LoginSubject subject) { if (logger.isInfoEnabled()) { logger.info("AuthorizationInfoGet -- getRoleSet -- subject = {}", subject); } // 自定义角色编码列表获取逻辑,下面的只是示例 Set roleSet = new HashSet() {{ add("admin"); add("user"); }}; return roleSet; } } ``` --- ## 2.2、会话认证 ### 2.2.1 登录认证,创建会话 ```java @RestController public class LoginController { @Autowired private AuthProvider authProvider; @PostMapping("/login") public Result 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 等等 --- ### 2.2.2 退出登录,注销会话 ```java @Controller public class IndexController { @Autowired private AuthProvider authProvider; @ResponseBody @GetMapping("/logout") public Result logout(HttpServletRequest request) { // 退出登录,注销会话 authProvider.logout(request); // 不传入request亦可,会自动获取当前的request // authProvider.logout(); return Result.ok("退出登录成功!"); } } ``` ### 2.2.3 获取当前登录会话 ```java @Autowired private AuthProvider authProvider; // 获取登录ID,无会话时会抛出异常 Object loginId = authProvider.getLoginId(); String loginIdStr = authProvider.getLoginIdAsString(); Long loginIdLong = authProvider.getLoginIdAsLong(); // 获取登录主体信息,无会话时会抛出异常 LoginSubject LoginSubject = authProvider.getLoginSubject(); ``` 也可使用静态工具类 `AuthUtil`: ```java // (这个方法在无会话时不会抛出异常,而是返回null),还可以直接getLoginIdAsString(), getLoginIdAsInt(), getLoginIdAsLong() Object loginId = AuthUtil.getLoginId(); // (这个方法在无会话时不会抛出异常,而是返回null) LoginSubject LoginSubject = AuthUtil.getLoginSubject(); ``` --- ### 2.2.4 获取当前登录用户token ```java @Autowired private AuthProvider authProvider; String token = authProvider.getToken(); // 或者 String token = authProvider.getToken(HttpServletRequest); ``` --- ### 2.2.5 获取当前登录用户凭证(对应redis或database里的唯一键) ```java @Autowired private AuthProvider authProvider; String credentials = authProvider.getCredentials(); // 或者 String credentials = authProvider.getCredentials(HttpServletRequest); ``` --- ### 2.2.6 使用会话验证忽略注解 `@Ignore` 在Controller的方法或类上面添加`@Ignore`注解可排除框架会话拦截,即表示调用接口不用传递token了。 --- ### 2.2.7 会话主动注销 ```java @Autowired private AuthProvider authProvider; // 根据token,使会话注销 authProvider.deleteByToken(token); // 根据会话凭证credentials,使会话注销 authProvider.deleteByCredentials(credentials); // 根据用户loginId,使该用户的全部会话都注销 authProvider.deleteTokenByLoginId(loginId); ``` --- ## 2.3、权限认证 ### 2.3.1 注解方式控制权限和角色 **1.注解解释:** ```java // 需要有 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.代码示例:** ```java @Controller public class IndexController { final static Logger logger = LoggerFactory.getLogger(IndexController.class); @Autowired private AuthProvider authProvider; @RequiresPermissions("权限3") @ResponseBody @GetMapping("/testPermission") public Result testPermission() { return Result.ok("testPermission测试成功!"); } @RequiresRoles(value="角色1") @ResponseBody @GetMapping("/testRole") public Result 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()); } } ``` --- ### 2.3.2 代码方式手动控制权限和角色 **1.代码示例:** ```java // 判断:当前账号是否含有指定角色, 返回 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"); ``` ### 2.3.3 直接通过URL控制权限(不支持角色校验) PermissionInfoInterfaceImpl实现类里返回的权限编码要和接口URL相匹配(需要带上context-path) --- ### 2.3.3 权限通配符的使用 > 🚨支持使用通配符指定泛权限,例如当一个账号拥有system:user:*的权限时,system:user:add、system:user:delete、system:user:update都将匹配通过 > ⚠️注意 > 当一个账号拥有 `*` 权限时,可以验证通过任何权限码 (角色认证同理), 所以请谨慎使用 `*` 权限码 --- ## 2.4、异常处理 tiny-security在会话验证失败和权限验证失败的会抛出自定义异常: | 自定义异常 | 描述 | 错误信息 | |:----------------------|:-------------|:-------------------------| | TinySecurityException | 基础异常 | 错误信息“系统异常!”,错误码500 | | UnAuthorizedException | 未登录或会话已失效 | 错误信息“未登录或会话已失效!”,错误码401 | | NoPermissionException | 无权限访问(角色或者资源不匹配) | 错误信息“无权限访问!”,错误码403 | | ConcurrentLoginOverLimitException | 并发登录超过限制 | 错误信息“并发登录超过最大限制!”,错误码409 | 需要使用全局异常处理器来捕获异常并进行处理返回JSON数据(或者页面): ```java @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())); } } } ``` --- ## 2.5、其他更多用法 ### 2.5.1 前端传递token 1. 放在参数里面用`token`传递: ```javascript $.get("/xxx", { "token": token }, function(data) { }); ``` 2. 放在header里面用`token`传递: ```javascript $.ajax({ url: "/xxx", beforeSend: function(xhr) { xhr.setRequestHeader("token", token); }, success: function(data){ } }); ``` 3. 前后端不分离的项目会自动从cookie里获取`token` --- ### 2.5.2 自定义AuthProvider 框架内置了JdbcAuthProvider、RedisAuthProvider和SingleAuthProvider三种会话实现, 如果仍然无法满足你的需求,或者你想存在其他什么地方,比如存在磁盘文件、MongoDB中,只需以下三步即可: - 继承org.tinycloud.security.provider.AbstractAuthProvider抽象类, 实现里面的抽象方法, - 注入bean,如下 ```java @Component @ConditionalOnProperty(name = "tiny-security.store-type", havingValue = "mongo") public class MongoAuthProvider extends AbstractAuthProvider { // ... } ``` - 配置 ```yaml tiny-security: store-type: mongo ``` ### 2.5.3 密码加密算法 框架封装了一些常见的加密算法,可供使用 1. 摘要算法: 支持MD5、SHA256和国密SM3算法 ```java 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(); ``` 2. 对称加密 支持AES256-CBC算法 ```java // 原文: 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); ``` 3. 非对称加密 支持RSA2048加密 ```java Map 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); ``` 4. 密码哈希算法 支持BCrypt算法 ```java // 密码哈希 String hashedPassword = BCrypt.hashpw("123456", BCrypt.gensalt()); System.out.println(hashedPassword); // 密码校验 boolean isPasswordMatch = BCrypt.checkpw("123456", hashedPassword); System.out.println(isPasswordMatch); ```