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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 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
相关文章
|
11天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
554 6
|
7天前
|
安全 Java 开发者
Java修饰符与封装:理解访问权限、行为控制与数据隐藏的重要性
Java中的修饰符和封装概念是构建健壯、易维护和扩展的Java应用程序的基石。通过合理利用访问权限修饰符和非访问修饰符,开发者能够设计出更加安全、灵活且高效的代码结构。封装不仅是面向对象编程的核心原则之一,也是提高软件项目质量和可维护性的关键策略。
10 1
|
10天前
|
算法 Java
Java项目不使用框架如何实现限流?
Java项目不使用框架如何实现限流?
20 2
|
15天前
|
Kubernetes Java Android开发
用 Quarkus 框架优化 Java 微服务架构的设计与实现
Quarkus 是专为 GraalVM 和 OpenJDK HotSpot 设计的 Kubernetes Native Java 框架,提供快速启动、低内存占用及高效开发体验,显著优化了 Java 在微服务架构中的表现。它采用提前编译和懒加载技术实现毫秒级启动,通过优化类加载机制降低内存消耗,并支持多种技术和框架集成,如 Kubernetes、Docker 及 Eclipse MicroProfile,助力开发者轻松构建强大微服务应用。例如,在电商场景中,可利用 Quarkus 快速搭建商品管理和订单管理等微服务,提升系统响应速度与稳定性。
31 5
|
16天前
|
机器学习/深度学习 数据采集 JavaScript
ADR智能监测系统源码,系统采用Java开发,基于SpringBoot框架,前端使用Vue,可自动预警药品不良反应
ADR药品不良反应监测系统是一款智能化工具,用于监测和分析药品不良反应。该系统通过收集和分析病历、处方及实验室数据,快速识别潜在不良反应,提升用药安全性。系统采用Java开发,基于SpringBoot框架,前端使用Vue,具备数据采集、清洗、分析等功能模块,并能生成监测报告辅助医务人员决策。通过集成多种数据源并运用机器学习算法,系统可自动预警药品不良反应,有效减少药害事故,保障公众健康。
ADR智能监测系统源码,系统采用Java开发,基于SpringBoot框架,前端使用Vue,可自动预警药品不良反应
|
1月前
|
Java 数据库连接 Apache
Java进阶-主流框架总结与详解
这些仅仅是 Java 众多框架中的一部分。每个框架都有其特定的用途和优势,了解并熟练运用这些框架,对于每一位 Java 开发者来说都至关重要。同时,选择合适框架的关键在于理解框架的设计哲学、核心功能及其在项目中的应用场景。随着技术的不断进步,这些框架也在不断更新和迭代以适应新的开发者需求。
39 1
|
存储 Java
《21天学通Java(第7版)》—— 2.10 认证练习
下面的问题是Java认证考试中可能出现的问题,请回答该问题,而不要查看本章的内容。
1394 0
|
Java
《21天学通Java(第7版)》—— 1.9 认证练习
下面的问题是Java认证考试中可能出现的问题,请回答该问题,而不要查看本章的内容。 下面的哪些说法是正确的? A.使用同一个类创建的所有对象都必须相同 B.使用同一个类创建的对象可以有不同的属性 C.对象将继承用于创建它的类的属性和行为 D.类将继承其超类的属性和行为
1416 0
下一篇
无影云桌面