Skip to content

一个基于token验证的Java Web权限控制框架,支持redis、jdbc和单机session多种存储方式,前后端分离项目、不分离项目均可使用,功能完善、使用简单、文档清晰,易于扩展。

License

Notifications You must be signed in to change notification settings

llllllxy/tiny-security

Repository files navigation

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

<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>

2.1.3 配置参数

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

2.1.4 会话存储配置

  • store-type配置为jdbc时,需要配置数据库连接信息,并导入框架提供的sql脚本到数据库中(目前仅提供了MySQL版本)
  • store-type配置为redis时,需要配置redis连接信息
  1. 使用jdbc做会话存储容器

依赖于jdbcTemplate,须导入依赖 spring-boot-starter-jdbc,在yml里进行数据库连接的相应配置并导入框架提供的sql脚本(目前仅提供了MySQL版本)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
  1. 使用redis做会话存储容器

依赖于stringRedisTemplate,须导入依赖 spring-boot-starter-data-redis ,并在yml里进行redis连接的相应配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.1.5 实现AuthorizationInfoGet接口

如需开启权限(角色)校验,还需要实现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;
    }
}

2.2、会话认证

2.2.1 登录认证,创建会话

@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 等等

2.2.2 退出登录,注销会话

@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("退出登录成功!");
    }
}

2.2.3 获取当前登录会话

@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();

2.2.4 获取当前登录用户token

@Autowired
private AuthProvider authProvider;

String token = authProvider.getToken();
// 或者
String token = authProvider.getToken(HttpServletRequest);

2.2.5 获取当前登录用户凭证(对应redis或database里的唯一键)

@Autowired
private AuthProvider authProvider;

String credentials = authProvider.getCredentials();
// 或者
String credentials = authProvider.getCredentials(HttpServletRequest);

2.2.6 使用会话验证忽略注解 @Ignore

在Controller的方法或类上面添加@Ignore注解可排除框架会话拦截,即表示调用接口不用传递token了。


2.2.7 会话主动注销

@Autowired
private AuthProvider authProvider;

// 根据token,使会话注销
authProvider.deleteByToken(token);

// 根据会话凭证credentials,使会话注销
authProvider.deleteByCredentials(credentials);

// 根据用户loginId,使该用户的全部会话都注销
authProvider.deleteTokenByLoginId(loginId);

2.3、权限认证

2.3.1 注解方式控制权限和角色

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());
    }
}

2.3.2 代码方式手动控制权限和角色

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");

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数据(或者页面):

@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传递:
$.get("/xxx", { "token": token }, function(data) {

});
  1. 放在header里面用token传递:
$.ajax({
   url: "/xxx", 
   beforeSend: function(xhr) {
       xhr.setRequestHeader("token", token);
   },
   success: function(data){ }
});
  1. 前后端不分离的项目会自动从cookie里获取token

2.5.2 自定义AuthProvider

框架内置了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

2.5.3 密码加密算法

框架封装了一些常见的加密算法,可供使用

  1. 摘要算法: 支持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();
  1. 对称加密 支持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);
  1. 非对称加密 支持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);
  1. 密码哈希算法 支持BCrypt算法
    // 密码哈希
    String hashedPassword = BCrypt.hashpw("123456", BCrypt.gensalt());
    System.out.println(hashedPassword);

    // 密码校验
    boolean isPasswordMatch = BCrypt.checkpw("123456", hashedPassword);
    System.out.println(isPasswordMatch);

About

一个基于token验证的Java Web权限控制框架,支持redis、jdbc和单机session多种存储方式,前后端分离项目、不分离项目均可使用,功能完善、使用简单、文档清晰,易于扩展。

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages