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

news2025/3/15 11:17:41

一、框架介绍

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级认证校验 —— 必须二级认证之后才能进入该方法。
@SaCheckBasicHttpBasic校验 —— 只有通过 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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1197159.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【网络开发必看】聊聊 Tomcat

文章目录 1. 什么是 Tomcat2. 怎么安装 Tomcat3. Tomcat 的目录结构3.1 bin 目录3.2 conf 目录3.3 lib 目录3.4 log 目录3.5 webapps 目录 4. 启动 Tomcat总结 1. 什么是 Tomcat Tomcat 是一个 HTTP 服务器. 前面学习了 HTTP 协议, 知道了 HTTP 协议就是规定 HTTP 客户端和 HT…

【C++】类型转换 | IO流 | 空间配置器

C语言类型转换 C语言总共有两种形式的类型转换&#xff1a;隐式类型转换 和 显示类型转换。 C语言的转换格式虽然很简单&#xff0c;但也存在不少缺陷&#xff1a; 隐式类型转换有些情况下可能会引发意料之外的结果&#xff0c;比如数据精度丢失。显示类型转换的可视性比较差…

MYSQL操作详解

一)计算机的基本结构 但是实际上&#xff0c;更多的是这种情况: 二)MYSQL中的数据类型: 一)数值类型: 数据类型内存大小(字节)说明bit(M)M指定位数,默认为1单个二进制位值&#xff0c;或者为0或者为1&#xff0c;主要用于开/关标志tinyint1字节1个字节的整数值&#xff0c;支持…

iPad系列将在2024年全面更新!

今年还会有新iPad发布吗&#xff1f;答案是否定的。因为早在前几天的季度电话会议上&#xff0c;苹果公司CEO蒂姆・库克就已经宣布&#xff0c;今年不会推出任何新的iPad产品。 这也意味着&#xff0c;今年将是苹果公司自2010年推出首款iPad设备以来&#xff0c;第一次没有发布…

【LeetCode刷题日志】138.随机链表的复制

&#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;C/C领域新星创作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;LeetCode 刷题日志&#x1f91d;希望作者的文章能对你有所帮助&#xff0c;有不足的地方请在评论区留言指正&#xff0c;…

穿越内存迷宫:C语言地址与指针的的冒险之旅

前言 C语言的魅力在于其直接的内存控制&#xff0c;而地址和指针是这种控制的核心。深入了解这些概念&#xff0c;将帮助我们更好地理解和利用C语言的潜力。本文将带领你踏上地址与指针的奇妙之旅&#xff0c;揭示它们在程序设计中的神秘面纱。 奇妙指针之旅&#xff1a;解码…

好物周刊#30:Github 上大学

https://github.com/cunyu1943/JavaPark https://yuque.com/cunyu1943 村雨遥的好物周刊&#xff0c;记录每周看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;每周五发布。 一、项目 1. Fighting Design 一款灵活、优质的组件库&#xff0c;可在 vue3 应用程…

C语言 音乐播放器项目(综合)

1.main.c文件 #include<stdio.h> #include<stdlib.h> #include<string.h> #include <unistd.h>//休眠所需的头文件 #include "./pos/console.h"//光标使用所需的头文件 #include "lrc.h" #include "./mplayer/start_mplayer…

让你的win10/win11系统变得不再卡顿,优雅草伊凡整理-长期更新-如何让windows操作系统不用老是重装在不断的更新中依然保持流畅运行

概述 如题&#xff1a;让你的win10/win11系统变得不再卡顿&#xff0c;优雅草伊凡整理-长期更新-如何让windows操作系统不用老是重装在不断的更新中依然保持流畅运行 本文长期更新&#xff0c;本次更新2023年11月8日&#xff01; 很多时候 我们的win10win11系统不管再怎么关…

基于SSM的软考系统设计实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

Servlet作业小练习

一.题目 利用JavaBean实现用户类&#xff0c;包含姓名、性别、爱好&#xff0c;爱好需要用多选框 实现表单1进行获取数据&#xff0c;表单2显示获取结果。 利用Servlet实现逻辑代码 二.实现效果 三.具体实现 1.User实体类 package com.hjj.pojo;/*** author:嘉佳 Date:20…

【蓝桥杯软件赛 零基础备赛20周】第3周——填空题

报名明年4月蓝桥杯软件赛的同学们&#xff0c;如果你是大一零基础&#xff0c;目前懵懂中&#xff0c;不知该怎么办&#xff0c;可以看看本博客系列&#xff1a;备赛20周合集 20周的完整安排请点击&#xff1a;20周计划 文章目录 00. 2023年第14届参赛数据0. 上一周答疑1. 填空…

环境变量小节

这是写的第二篇环境变量博客&#xff0c;写了一年多了&#xff0c;第一次出现把自己博客删了的情况&#xff0c;不知道为什么明明发表了&#xff0c;然后就把草稿箱和回收站的删了&#xff0c;结果晚上发现没发表&#xff0c;回收站删除是无法找回的&#xff0c;以后还是要慎重…

酷柚易汛ERP-账户管理操作指南

1、应用场景 对账户进行管理&#xff0c;可设置账户当前余额、期初余额和设置是否为默认账户。 2、主要操作 2.1 新增支付账户 打开【资料】-【账款管理】&#xff0c;点击【新增】添加账户类别&#xff0c;输入相关信息并保存&#xff0c;账户编号和名称为必录项。&#x…

VirtualBox网络地址转换(NAT),宿主机无法访问虚拟机的问题

问题&#xff1a;NAT模式下&#xff0c;默认只能从内访问外面&#xff0c;而不能从外部访问里面&#xff0c;所以只能单向ping通&#xff0c;虚拟机的ip只是内部ip。 PS&#xff1a;桥接则是与主机公用网卡&#xff0c;有独立的外部ip。 解决&#xff1a;NAT模式可以通过配置 …

最简WebClient 同步、异步调用示例

目录 一&#xff0c;序言二&#xff0c;简单示例1. 引入依赖2. 日志配置3. 调用代码4. 运行结果 三&#xff0c;完整代码 一&#xff0c;序言 WebClient是Spring WebFlux模块提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具&#xff0c;从Spring5.0开始WebClient…

【JavaEE初阶】 TCP协议详细解析

文章目录 &#x1f332;TCP协议的概念&#x1f6a9;TCP协议段格式&#x1f6a9;TCP的特性 &#x1f333;TCP原理&#x1f6a9;确认应答机制&#xff08;安全机制&#xff09;&#x1f6a9;超时重传机制&#xff08;安全机制&#xff09;&#x1f6a9;三次握手四次挥手&#xff…

【蓝桥杯选拔赛真题65】Scratch水下探险 少儿编程scratch图形化编程 蓝桥杯创意编程选拔赛真题解析

目录 scratch水下探险 一、题目要求 编程实现 二、案例分析 1、角色分析

概念解析 | Richardson-Lucy去卷积算法

注1:本文系“概念解析”系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:Richardson-Lucy去模糊算法 Richardson-Lucy去模糊算法:重现图像的真实面目 Blind deconvolution by means of the Richardson–Lucy algorithm 背景介绍 在图像处理中,图像获取…

jQuery实现二级菜单

jQuery怎么实现二级菜单呢&#xff1f;让我为大家演示一个例子&#xff01; 上代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title></title><style>* {margin: 0;padding: …