一个轻量级 Java 权限认证框架——Sa-Token

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。

一、框架介绍

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

官网文档:

https://sa-token.cc/doc.html

二、Spring Boot 集成Sa-Token

2.1、创建Spring Boot工程

创建一个xxkfz-sa-token项目

2.2、添加依赖

由于本项目工程使用Spring Boot3.1.5版本;maven需要添加以下的依赖:

<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot3-starter</artifactId>
    <version>1.37.0</version>
</dependency>

注:非SpringBoot 3.x 版本:只需要sa-token-spring-boot3-starter 修改为sa-token-spring-boot-starter。

Sa-Token 默认是将数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如:

  • 重启后数据会丢失。
  • 无法在分布式环境中共享数据。

集成Redis,添加如下依赖:

  <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
     <groupId>cn.dev33</groupId>
     <artifactId>sa-token-redis-jackson</artifactId>
     <version>1.37.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

注:集成 Redis 只需要引入对应的 pom依赖 即可,框架所有上层 API 保持不变。数据是框架自动的做保存。

完整的pom.xml内容如下:

pom.xml

         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot3-starter</artifactId>
            <version>1.37.0</version>
        </dependency>
        <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-redis-jackson</artifactId>
            <version>1.37.0</version>
        </dependency>
        <!-- 提供Redis连接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.2</version>
        </dependency>

        <!--引入mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>

2.3、配置文件添加配置

application.yml

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: satoken
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true


spring:
  application:
    name: xxkfz-sa
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/xxkfz_sa_token?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: xxkfz
    password: xxkfz



  # redis配置
  redis:
    # Redis数据库索引(默认为0)
    database: 1
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    # password:
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0



logging:
  level:
    com:
      xxkfz:
        simplememory:
          mapper: info
    root: info
  pattern:
    console: '%p%m%n'
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

2.4、创建启动类、及代码基本结构

XxkfzSaTokenApplication.java

@SpringBootApplication
@Slf4j
@MapperScan("com.xxkfz.simplememory.mapper")
public class XxkfzSaTokenApplication {
   
   

    public static void main(String[] args) {
   
   
        SpringApplication.run(XxkfzSaTokenApplication.class, args);
        log.error("启动成功,Sa-Token 配置如下:{}", SaManager.getConfig());

    }
}

2.5、启动项目

至此,项目基本的结构搭建完成!

三、Sa-Token基础使用

3.1、登录认证

对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:

  • 如果校验通过,则:正常返回数据。
  • 如果校验未通过,则:抛出异常,告知其需要先进行登录。

    那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:

  1. 用户提交 name + password 参数,调用登录接口。
  2. 登录成功,返回这个用户的 Token 会话凭证。
  3. 用户后续的每次请求,都携带上这个 Token。
  4. 服务器根据 Token 判断此会话是否登录成功。

所谓登录认证,指的就是服务器校验账号密码,为用户颁发Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。

Sa-Token登录认证

3.2、会话的登录注销查询

以下接口主要包含了:会话的登录、注销、查询以及Token的查询函数演示示例。

/**
 * @program: xxkfz-sa-token
 * @ClassName UserController.java
 * @author: xxkfz
 * @create: 2023-11-07 15:06
 * @description: 用户登录、注销、会话查询演示
 * @Version 1.0
 **/
@RestController
@RequestMapping("/user/")
@Slf4j
public class UserController {
   
   

    /**
     * 登录:  http://localhost:8089/user/doLogin?username=xxkfz&password=123456
     *
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("doLogin")
    public SaResult doLogin(String username, String password) {
   
   
        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
        if ("xxkfz".equals(username) && "123456".equals(password)) {
   
   
            StpUtil.login(10001);
            return SaResult.ok("登录成功");
        }
        return SaResult.error("登录失败");
    }

    /**
     * 获取当前会话是否已经登录  返回true=已登录,false=未登录
     * http://localhost:8089/user/
     *
     * @return
     */
    @RequestMapping("isLogin")
    public String isLogin() {
   
   
        return "当前会话是否登录:" + StpUtil.isLogin();
    }

    /**
     * 检查当前会话是否已经登录 如果未登录,则抛出异常:`NotLoginException`
     *
     * @returnn
     */
    @GetMapping("checkLogin")
    public String checkLogin() {
   
   
        StpUtil.checkLogin();
        return "";
    }

    /**
     * 当前会话注销登录
     *
     * @return
     */
    @GetMapping("logout")
    public String logout() {
   
   
        StpUtil.logout();
        return "已注销";
    }

    /**
     * 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
     *
     * @return
     */
    @GetMapping("getLoginId")
    public String getLoginId() {
   
   
        Object loginId = StpUtil.getLoginId();
        String loginIdAsString = StpUtil.getLoginIdAsString();// 获取当前会话账号id, 并转化为`String`类型
        int loginIdAsInt = StpUtil.getLoginIdAsInt();// 获取当前会话账号id, 并转化为`int`类型
        long loginIdAsLong = StpUtil.getLoginIdAsLong();// 获取当前会话账号id, 并转化为`long`类型
        String loginIdAsDefault = StpUtil.getLoginId("未登录"); //  获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
        log.error("当前会话账号id = {}", loginIdAsString);
        log.error("当前会话账号id = {}", loginIdAsInt);
        log.error("当前会话账号id = {}", loginIdAsLong);
        log.error("当前会话账号id = {}", loginIdAsDefault);
        return "当前会话账号id: " + loginId.toString();
    }

    /**
     * 查询Token信息
     *
     * @return
     */
    @RequestMapping("tokenInfo")
    public SaResult tokenInfo() {
   
   
        // TokenName 是 Token 名称的意思,此值也决定了前端提交 Token 时应该使用的参数名称
        String tokenName = StpUtil.getTokenName();
        System.out.println("前端提交 Token 时应该使用的参数名称:" + tokenName);

        // 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值
        // 框架默认前端可以从以下三个途径中提交 Token:
        //         Cookie         (浏览器自动提交)
        //         Header头    (代码手动提交)
        //         Query 参数    (代码手动提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx
        // 读取顺序为: Query 参数 --> Header头 -- > Cookie
        // 以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token
        String tokenValue = StpUtil.getTokenValue();
        System.out.println("前端提交的Token值为:" + tokenValue);

        // TokenInfo 包含了此 Token 的大多数信息
        SaTokenInfo info = StpUtil.getTokenInfo();
        System.out.println("Token 名称:" + info.getTokenName());
        System.out.println("Token 值:" + info.getTokenValue());
        System.out.println("当前是否登录:" + info.getIsLogin());
        System.out.println("当前登录的账号id:" + info.getLoginId());
        System.out.println("当前登录账号的类型:" + info.getLoginType());
        System.out.println("当前登录客户端的设备类型:" + info.getLoginDevice());
        System.out.println("当前 Token 的剩余有效期:" + info.getTokenTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
        System.out.println("当前 Token 距离被冻结还剩:" + info.getTokenActiveTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
        System.out.println("当前 Account-Session 的剩余有效期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
        System.out.println("当前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在

        // 返回给前端
        return SaResult.data(StpUtil.getTokenInfo());
    }
}

下面是一些简单的演示:

由于我们上述已经集成Redis,相关的会话信息会存储在Redis中。

访问:

http://localhost:8082/user/doLogin?username=xxkfz&password=123456

我们可以看到控制台登录成功,同时成功生成Token信息。

数据存储在Redis。

访问:

http://localhost:8082/user/tokenInfo

查询Token信息。

{
   
   
    "code": 200,
    "msg": "ok",
    "data": {
   
   
        "tokenName": "satoken",
        "tokenValue": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjEwMDAxLCJyblN0ciI6Ik1hYVBIVkJNNENDYllGVHBxdFU4NmNvVTRQcEM0cm9UIn0.oQx2R0d5KnFbeXLDfl-nOCdtunBSqknU2wWOu0PQcm0",
        "isLogin": true,
        "loginId": "10001",
        "loginType": "login",
        "tokenTimeout": 2591997,
        "sessionTimeout": 2591997,
        "tokenSessionTimeout": -2,
        "tokenActiveTimeout": -1,
        "loginDevice": "default-device",
        "tag": null
    }
}

访问:

http://localhost:8082/user/logout

注销会话,同时Redis会话数据将会被删除。

3.3、权限角色的校验

所谓的权限认证,核心逻辑就是判断一个账号是否拥有指定的权限:

  • 有,就让你通过。
  • 没有?那么禁止访问!

深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。

例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"],这时候我来校验权限 "user-update",则其结果就是:验证失败,禁止访问

Sa-Token权限认证

获取当前账号的权限码集合

因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以 Sa-Token 将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。

新建一个类,实现StpInterface接口,实现以下两个方法:

// 返回一个账号所拥有的权限码集合
List<String> getPermissionList(Object loginId, String loginType);
// 返回一个账号所拥有的角色标识集合
List<String> getRoleList(Object loginId, String loginType)

示例:

/**
 * 自定义权限加载接口实现类
 */
@Component    // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
   
   

    /**
     * 返回一个账号所拥有的权限码集合
     *
     * @param loginId   账号id,即你在调用 StpUtil.login(id) 时写入的标识值。
     * @param loginType 账号体系标识,此处可以暂时忽略,在 [ 多账户认证 ] 章节下会对这个概念做详细的解释。
     * @return
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
   
   
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
        List<String> list = new ArrayList<>();
        list.add("101");
        list.add("user.add");
        list.add("user.update");
        list.add("user.get");
//         list.add("user.delete");
        list.add("art.*");
        return list;
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
   
   
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
        List<String> list = new ArrayList<String>();
        list.add("admin");
        list.add("super-admin");
        return list;
    }
}

参数解释:

  • loginId:账号id,即你在调用 StpUtil.login(id) 时写入的标识值。

  • loginType:账号体系标识,此处可以暂时忽略,在 [ 多账户认证 ] 章节下会对这个概念做详细的解释。

权限校验

// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();

// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.add");        

// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException 
StpUtil.checkPermission("user.add");        

// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");        

// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");

角色校验

在 Sa-Token 中,角色和权限可以分开独立验证

// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();

// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin");        

// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");        

// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");        

// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] 
StpUtil.checkRoleOr("super-admin", "shop-admin");

权限通配符

// 当拥有 art.* 权限时
StpUtil.hasPermission("art.add");        // true
StpUtil.hasPermission("art.update");     // true
StpUtil.hasPermission("goods.add");      // false

// 当拥有 *.delete 权限时
StpUtil.hasPermission("art.delete");      // true
StpUtil.hasPermission("user.delete");     // true
StpUtil.hasPermission("user.update");     // false

// 当拥有 *.js 权限时
StpUtil.hasPermission("index.js");        // true
StpUtil.hasPermission("index.css");       // false
StpUtil.hasPermission("index.html");      // false

注:上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码 (角色认证同理)。

代码示例

@RestController
@RequestMapping("/auth/")
@Slf4j
public class UserAuthController {
   
   

    /**
     * 查询权限
     *
     * @return
     */
    @RequestMapping("getPermission")
    public SaResult getPermission() {
   
   
        // 查询权限信息 ,如果当前会话未登录,会返回一个空集合
        List<String> permissionList = StpUtil.getPermissionList();
        System.out.println("当前登录账号拥有的所有权限:" + permissionList);

        // 查询角色信息 ,如果当前会话未登录,会返回一个空集合
        List<String> roleList = StpUtil.getRoleList();
        System.out.println("当前登录账号拥有的所有角色:" + roleList);

        // 返回给前端
        return SaResult.ok().set("roleList", roleList).set("permissionList", permissionList);
    }


    /**
     * 权限校验
     *
     * @return
     */
    @RequestMapping("checkPermission")
    public SaResult checkPermission() {
   
   

        // 判断:当前账号是否拥有一个权限,返回 true 或 false
        //         如果当前账号未登录,则永远返回 false
        StpUtil.hasPermission("user.add");
        StpUtil.hasPermissionAnd("user.add", "user.delete", "user.get");  // 指定多个,必须全部拥有才会返回 true
        StpUtil.hasPermissionOr("user.add", "user.delete", "user.get");     // 指定多个,只要拥有一个就会返回 true

        // 校验:当前账号是否拥有一个权限,校验不通过时会抛出 `NotPermissionException` 异常
        //         如果当前账号未登录,则永远校验失败
        StpUtil.checkPermission("user.add");
        StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");  // 指定多个,必须全部拥有才会校验通过
        StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");  // 指定多个,只要拥有一个就会校验通过

        return SaResult.ok();
    }

    /**
     * 角色校验
     *
     * @return
     */
    @RequestMapping("checkRole")
    public SaResult checkRole() {
   
   

        // 判断:当前账号是否拥有一个角色,返回 true 或 false
        //         如果当前账号未登录,则永远返回 false
        StpUtil.hasRole("admin");
        StpUtil.hasRoleAnd("admin", "ceo", "cfo");  // 指定多个,必须全部拥有才会返回 true
        StpUtil.hasRoleOr("admin", "ceo", "cfo");      // 指定多个,只要拥有一个就会返回 true

        // 校验:当前账号是否拥有一个角色,校验不通过时会抛出 `NotRoleException` 异常
        //         如果当前账号未登录,则永远校验失败
        StpUtil.checkRole("admin");
        StpUtil.checkRoleAnd("admin", "ceo", "cfo");  // 指定多个,必须全部拥有才会校验通过
        StpUtil.checkRoleOr("admin", "ceo", "cfo");  // 指定多个,只要拥有一个就会校验通过

        return SaResult.ok();
    }

    /**
     * 权限通配符
     *
     * @return
     */
    @RequestMapping("wildcardPermission")
    public SaResult wildcardPermission() {
   
   

        // 前提条件:在 StpInterface 实现类中,为账号返回了 "art.*" 泛权限
        StpUtil.hasPermission("art.add");  // 返回 true
        StpUtil.hasPermission("art.delete");  // 返回 true
        StpUtil.hasPermission("goods.add");  // 返回 false,因为前缀不符合

        // * 符合可以出现在任意位置,比如权限码的开头,当账号拥有 "*.delete" 时
        StpUtil.hasPermission("goods.add");        // false
        StpUtil.hasPermission("goods.delete");     // true
        StpUtil.hasPermission("art.delete");      // true

        // 也可以出现在权限码的中间,比如当账号拥有 "shop.*.user" 时
        StpUtil.hasPermission("shop.add.user");  // true
        StpUtil.hasPermission("shop.delete.user");  // true
        StpUtil.hasPermission("shop.delete.goods");  // false,因为后缀不符合

        // 注意点:
        // 1、上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码
        // 2、角色校验也可以加 * ,指定泛角色,例如: "*.admin",暂不赘述

        return SaResult.ok();
    }
}

拦截全局异常

鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?当然不可以!

下面是创建一个全局异常拦截器,统一返回给前端的格式。

@RestControllerAdvice
public class GlobalExceptionHandler {
   
   
    // 拦截:未登录异常
    @ExceptionHandler(NotLoginException.class)
    public SaResult handlerException(NotLoginException e) {
   
   

        // 打印堆栈,以供调试
        e.printStackTrace();

        // 返回给前端
        return SaResult.error(e.getMessage());
    }

    // 拦截:缺少权限异常
    @ExceptionHandler(NotPermissionException.class)
    public SaResult handlerException(NotPermissionException e) {
   
   
        e.printStackTrace();
        return SaResult.error("缺少权限:" + e.getPermission());
    }

    // 拦截:缺少角色异常
    @ExceptionHandler(NotRoleException.class)
    public SaResult handlerException(NotRoleException e) {
   
   
        e.printStackTrace();
        return SaResult.error("缺少角色:" + e.getRole());
    }

    // 拦截:二级认证校验失败异常
    @ExceptionHandler(NotSafeException.class)
    public SaResult handlerException(NotSafeException e) {
   
   
        e.printStackTrace();
        return SaResult.error("二级认证校验失败:" + e.getService());
    }

    // 拦截:服务封禁异常
    @ExceptionHandler(DisableServiceException.class)
    public SaResult handlerException(DisableServiceException e) {
   
   
        e.printStackTrace();
        return SaResult.error("当前账号 " + e.getService() + " 服务已被封禁 (level=" + e.getLevel() + "):" + e.getDisableTime() + "秒后解封");
    }

    // 拦截:Http Basic 校验失败异常
    @ExceptionHandler(NotBasicAuthException.class)
    public SaResult handlerException(NotBasicAuthException e) {
   
   
        e.printStackTrace();
        return SaResult.error(e.getMessage());
    }

    // 拦截:其它所有异常
    @ExceptionHandler(Exception.class)
    public SaResult handlerException(Exception e) {
   
   
        e.printStackTrace();
        return SaResult.error(e.getMessage());
    }

}

比如我们在调用上面注销接口后,然后调用:http://localhost:8082/user/checkLogin 检查当前会话是否已经登录。

将会进入全局异常中类型为NotLoginException的异常处理器。

统一放回数据:

{
   
   
    "code": 500,
    "msg": "未能读取到有效 token",
    "data": null
}

3.4、注解鉴权

注解鉴权 —— 优雅的将鉴权与业务代码分离!

注解 说明
@SaCheckLogin 登录校验 —— 只有登录之后才能进入该方法。
@SaCheckRole("admin") 角色校验 —— 必须具有指定角色标识才能进入该方法。
@SaCheckPermission("user:add") 权限校验 —— 必须具有指定权限才能进入该方法。
@SaCheckSafe 级认证校验 —— 必须二级认证之后才能进入该方法。
@SaCheckBasic HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。
@SaIgnore 忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
@SaCheckDisable("comment") 账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。

配置注解式鉴权功能

Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中

注册 Sa-Token 拦截器,打开注解式鉴权功能

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
   
   
    // 注册 Sa-Token 拦截器,打开注解式鉴权功能

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   
   
        // 注册 Sa-Token 拦截器,打开注解式鉴权功能
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }
}

注解式鉴权使用

// 登录校验:只有登录之后才能进入该方法 
@SaCheckLogin                        
@RequestMapping("info")
public String info() {
   
   
    return "查询用户信息";
}

// 角色校验:必须具有指定角色才能进入该方法 
@SaCheckRole("super-admin")        
@RequestMapping("add")
public String add() {
   
   
    return "用户增加";
}

// 权限校验:必须具有指定权限才能进入该方法 
@SaCheckPermission("user-add")        
@RequestMapping("add")
public String add() {
   
   
    return "用户增加";
}

// 二级认证校验:必须二级认证之后才能进入该方法 
@SaCheckSafe()        
@RequestMapping("add")
public String add() {
   
   
    return "用户增加";
}

// Http Basic 校验:只有通过 Basic 认证后才能进入该方法 
@SaCheckBasic(account = "sa:123456")
@RequestMapping("add")
public String add() {
   
   
    return "用户增加";
}

// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 
@SaCheckDisable("comment")                
@RequestMapping("send")
public String send() {
   
   
    return "查询用户信息";
}

注:以上注解都可以加在类上,代表为这个类所有方法进行鉴权!

校验模式设定

@SaCheckRole@SaCheckPermission注解可设置校验模式,例如:

// 注解式鉴权:只要具有其中一个权限即可通过校验 
@RequestMapping("atJurOr")
@SaCheckPermission(value = {
   
   "user-add", "user-all", "user-delete"}, mode = SaMode.OR)        
public SaResult atJurOr() {
   
   
    return SaResult.data("用户信息");
}

mode有两种取值如下:

  • SaMode.AND,标注一组权限,会话必须全部具有才可通过校验。
  • SaMode.OR,标注一组权限,会话只要具有其一即可通过校验。

角色权限双重"or校验"

假设有以下业务场景:一个接口在具有权限 user.add 或角色 admin 时可以调通。怎么写?

// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")        
public SaResult userAdd() {
   
   
    return SaResult.data("用户信息");
}

orRole 字段代表权限校验未通过时的次要选择,两者只要其一校验成功即可进入请求方法,其有三种写法:

  • 写法一:orRole = "admin",代表需要拥有角色 admin 。
  • 写法二:orRole = {"admin", "manager", "staff"},代表具有三个角色其一即可。
  • 写法三:orRole = {"admin, manager, staff"},代表必须同时具有三个角色。

忽略认证

使用 @SaIgnore 可表示一个接口忽略认证:

@SaCheckLogin
@RestController
public class TestController {

    // ... 其它方法 

    // 此接口加上了 @SaIgnore 可以游客访问 
    @SaIgnore
    @RequestMapping("getList")
    public SaResult getList() {
        // ... 
        return SaResult.ok(); 
    }
}

如上代码表示:TestController 中的所有方法都需要登录后才可以访问,但是 getList 接口可以匿名游客访问。

  • @SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。
  • @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。
  • @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权。

3.5、路由拦截鉴权

假设我们项目中所有接口均需要登录认证,只有 “登录接口” 本身对外开放:

使用路由拦截器如下:

注册Sa-Token路由拦截器

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
   
   
    // 注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   
   
        // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
        registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
                .addPathPatterns("/**")
                .excludePathPatterns("/user/doLogin"); 
    }
}

在上面的代码中,注册了一个基于 StpUtil.checkLogin() 的登录校验拦截器,并且排除了/user/doLogin接口用来开放登录(除了/user/doLogin以外的所有接口都需要登录才能访问)。

检验函数

自定义认证规则:new SaInterceptor(handle -> StpUtil.checkLogin()) 是最简单的写法,代表只进行登录校验功能。

我们可以往构造函数塞一个完整的 lambda 表达式,来定义详细的校验规则,例如:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
   
   
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   
   
        // 注册 Sa-Token 拦截器,定义详细认证规则 
        registry.addInterceptor(new SaInterceptor(handler -> {
   
   
            // 指定一条 match 规则
            SaRouter
                .match("/**")    // 拦截的 path 列表,可以写多个 */
                .notMatch("/user/doLogin")        // 排除掉的 path 列表,可以写多个 
                .check(r -> StpUtil.checkLogin());        // 要执行的校验动作,可以写完整的 lambda 表达式

            // 根据路由划分模块,不同模块不同鉴权 
            SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
            SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
            SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
            SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
            SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
            SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
        })).addPathPatterns("/**");
    }
}

SaRouter.match() 匹配函数有两个参数:

  • 参数一:要匹配的path路由。
  • 参数二:要执行的校验函数。

3.6、Session会话

Session 是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据,提高程序性能,例如:

// 在登录时缓存 user 对象 
StpUtil.getSession().set("user", user);

// 然后我们就可以在任意处使用这个 user 对象
SysUser user = (SysUser) StpUtil.getSession().get("user");

在 Sa-Token 中,Session 分为三种,分别是:

  • Account-Session: 指的是框架为每个 账号id 分配的 Session
  • Token-Session: 指的是框架为每个 token 分配的 Session
  • Custom-Session: 指的是以一个 特定的值 作为SessionId,来分配的 Session

关于三者的详解:https://sa-token.cc/doc.html#/fun/session-model

Account-Session

有关 账号-Session 的 API 如下:

// 获取当前账号 id 的 Account-Session (必须是登录后才能调用)
StpUtil.getSession();

// 获取当前账号 id 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回
StpUtil.getSession(true);

// 获取账号 id 为 10001 的 Account-Session
StpUtil.getSessionByLoginId(10001);

// 获取账号 id 为 10001 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回
StpUtil.getSessionByLoginId(10001, true);

// 获取 SessionId 为 xxxx-xxxx 的 Account-Session, 在 Session 尚未创建时, 返回 null 
StpUtil.getSessionBySessionId("xxxx-xxxx");

Token-Session

有关 令牌-Session 的 API 如下:

// 获取当前 Token 的 Token-Session 对象
StpUtil.getTokenSession();

// 获取指定 Token 的 Token-Session 对象
StpUtil.getTokenSessionByToken(token);

Custom-Session

自定义 Session 指的是以一个特定的值作为 SessionId 来分配的Session, 借助自定义Session,你可以为系统中的任意元素分配相应的session
例如以商品 id 作为 key 为每个商品分配一个Session,以便于缓存和商品相关的数据,其相关API如下:

// 查询指定key的Session是否存在
SaSessionCustomUtil.isExists("goods-10001");

// 获取指定key的Session,如果没有,则新建并返回
SaSessionCustomUtil.getSessionById("goods-10001");

// 获取指定key的Session,如果没有,第二个参数决定是否新建并返回  
SaSessionCustomUtil.getSessionById("goods-10001", false);   

// 删除指定key的Session
SaSessionCustomUtil.deleteSessionById("goods-10001");

代码示例:

@RestController
@RequestMapping("/session/")
public class SaSessionController {
   
   

    /*
     * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述
     *         ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
     */

    // 简单存取值   ---- http://localhost:8081/session/getValue
    @RequestMapping("getValue")
    public SaResult getValue() {
   
   
        // 获取当前登录账号的专属 SaSession 对象
        //         注意点1:只有登录后才可以调用这个方法
        //        注意点2:每个账号获取到的都是不同的 SaSession 对象,存取值时不会互相影响
        //        注意点3:SaSession 和 HttpSession 是两个完全不同的对象,不可混淆使用
        SaSession session = StpUtil.getSession();

        // 存值
        session.set("name", "zhangsan");
        session.set("age", 18);

        // 取值
        Object name = session.get("name");
        String name2 = session.getString("name");   // 取值,并转化为 String 数据类型
        int age = session.getInt("age");    // 转 int 类型
        long age2 = session.getLong("age");    // 转 long 类型
        float age3 = session.getFloat("age");    // 转 float 类型
        double age4 = session.getDouble("age");    // 转 double 类型
        int age5 = session.get("age5", 22);  // 取不到时就返回默认值
        int age6 = session.get("age5", () -> {
   
     // 取不到时就执行 lambda 获取值
            return 26;
        });

        /*
         * 存取值范围是一次会话有效的,也就是说,在一次登录有效期内,你可以在一个请求里存值,然后在另一个请求里取值
         */

        List<Object> list = Arrays.asList(name, name2, age, age2, age3, age4, age5, age6);
        System.out.println(list);

        return SaResult.data(list);
    }

    // 复杂存取值   ---- http://localhost:8081/session/getModel
    @RequestMapping("getModel")
    public SaResult getModel() {
   
   
        // 实例化
        SysUser user = new SysUser();
        user.setId(10001);
        user.setName("张三");
        user.setAge(19);

        // 写入这个对象到 SaSession 中
        StpUtil.getSession().set("user", user);

        // 然后我们就可以在任意代码处获取这个 user 了
        SysUser user2 = StpUtil.getSession().getModel("user", SysUser.class);

        // 返回
        return SaResult.data(user2);
    }

    // 自定义Session   ---- http://localhost:8081/session/customSession
    @RequestMapping("customSession")
    public SaResult customSession() {
   
   

        // 自定义 Session 就是指使用一个特定的 key,来获取 Session 对象
        SaSession roleSession = SaSessionCustomUtil.getSessionById("role-1001");

        // 一样可以自由的存值写值
        roleSession.set("nnn", "lalala");
        System.out.println(roleSession.get("nnn"));

        // 返回
        return SaResult.ok();
    }
}

3.7、Sa-Token集成Jwt

pom.xml引入依赖

<!-- Sa-Token 整合 jwt -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-jwt</artifactId>
    <version>1.37.0</version>
</dependency>

配置密钥

sa-token:
    # jwt秘钥 
    jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk

注入Jwt实现

根据不同的整合规则,插件提供了三种不同的模式:

// Simple 简单模式
@Configuration
public class SaTokenConfigure {
   
   
    // Sa-Token 整合 jwt (Simple 简单模式)
    @Bean
    public StpLogic getStpLogicJwt() {
   
   
        return new StpLogicJwtForSimple();
    }
}


// Mixin 混入模式
@Configuration
public class SaTokenConfigure {
   
   
    // Sa-Token 整合 jwt (Mixin 混入模式)
    @Bean
    public StpLogic getStpLogicJwt() {
   
   
        return new StpLogicJwtForMixin();
    }
}


// Stateless 无状态模式
@Configuration
public class SaTokenConfigure {
   
   
    // Sa-Token 整合 jwt (Stateless 无状态模式)
    @Bean
    public StpLogic getStpLogicJwt() {
   
   
        return new StpLogicJwtForStateless();
    }
}

说明:在3.2章节中,项目已经提前集成了Jwt:访问:
http://localhost:8089/user/doLogin?username=xxkfz&password=123456
登录接口,可以看到生成的Token格式。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjEwMDAxLCJyblN0ciI6Ikk1N2REQ2NLc1hmbktGcDJ5emhubHRVcGk1RUlySEpHIn0.y_PFajeKjCwcxj1NOo7VAQg4Tbc7NAHI3SWAwqntRd4

关于有关Sa-Token 其他内容:https://sa-token.cc/doc.html

本文章代码工程:关注【小小开发者】私信即可

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
15小时前
|
分布式计算 前端开发 Java
Java的web框架
Java的web框架
|
1天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化移动应用开发的未来:跨平台框架与原生操作系统的融合
【5月更文挑战第29天】在Java开发中,并发编程是一个重要的议题。随着多核处理器的普及,如何充分利用多核资源,提高程序的执行效率,同时保证数据的安全性和一致性,成为开发者必须面对的挑战。本文将从线程安全的基本概念出发,探讨Java中的线程安全问题,并介绍一些常见的解决方案,如同步机制、锁优化等。最后,我们将通过实例分析,展示如何在保证线程安全的前提下,进行性能优化。
|
1天前
|
并行计算 算法 Java
探索Java并发编程:Fork/Join框架的深度解析
【5月更文挑战第29天】在多核处理器普及的时代,有效利用并发编程以提升程序性能已经成为开发者必须面对的挑战。Java语言提供的Fork/Join框架是一个强大的工具,它旨在利用多线程执行分而治之的任务。本文将通过深入分析Fork/Join框架的工作原理、关键特性以及与传统线程池技术的差异,帮助开发者更好地掌握这一高效处理并发任务的技术手段。
|
2天前
|
存储 Java 容器
初识Java集合框架
初识Java集合框架
|
5天前
|
Java 开发者
探索Java并发编程:Fork/Join框架的深度解析
【5月更文挑战第25天】在多核处理器日益普及的今天,并发编程成为了提升应用性能的关键。Java语言提供了多种并发工具,其中Fork/Join框架是一个高效且强大的工具,用于处理分而治之的任务。本文将深入探讨Fork/Join框架的原理、使用及其在实际应用中的优化策略,旨在帮助开发者更好地利用这一框架以解决复杂的并发问题。
|
Java Spring 容器
java轻量级IOC框架Guice
Guice是由Google大牛Bob lee开发的一款绝对轻量级的java IoC容器。其优势在于: 速度快,号称比spring快100倍。 无外部配置(如需要使用外部可以可以选用Guice的扩展包),完全基于annotation特性,支持重构,代码静态检查。
1740 0
|
1天前
|
安全 Java 调度
Java并发编程:线程池深入解析
【5月更文挑战第29天】本文将详细探讨Java中的线程池技术,包括其基本原理、使用方法以及在实际开发中的应用。我们将通过实例代码和性能分析,深入理解线程池的优势和潜在问题,为高效、安全的并发编程提供指导。
|
1天前
|
消息中间件 Java
Java中的多线程编程实践
【5月更文挑战第29天】本文主要介绍了Java中的多线程编程实践,包括线程的创建、启动、同步和通信等方面。通过对多线程编程的深入探讨,帮助读者更好地理解和掌握Java多线程编程的技巧和方法。
|
1天前
|
安全 Java API
Java多线程编程的最佳实践
在当今软件开发领域,多线程编程已经成为了一种必不可少的技能。本文将探讨Java多线程编程的最佳实践,讨论如何利用Java提供的丰富工具和技术来编写高效、安全和可靠的多线程程序。从线程生命周期管理、共享资源处理到并发控制,我们将介绍一系列最佳实践,帮助读者更好地应对多线程编程中的挑战。
|
1天前
|
监控 Java 开发者
深入理解Java并发编程:线程池的工作原理与实践
【5月更文挑战第29天】 在现代Java应用开发中,高效地管理并发任务是至关重要的。本文将深入探讨Java线程池的核心机制,揭示其背后的设计哲学和运作模式。通过分析线程池的优势、工作过程及关键参数,结合实例演示如何合理配置和优化线程池以提高应用程序的性能和响应能力。