当前位置:   article > 正文

Sa-Token 实现分布式登录鉴权(轻量级,超简单)

sa-token

点击关注公众号,利用碎片时间学习

1. Sa-Token 介绍

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

功能结构图

008fcec7e43050aaccf55637e5d5e05c.png

2. 登录认证

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

  • 如果校验通过,则:正常返回数据。

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

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

  • 用户提交 name + password 参数,调用登录接口。

  • 登录成功,返回这个用户的 Token 会话凭证。

  • 用户后续的每次请求,都携带上这个 Token。

  • 服务器根据 Token 判断此会话是否登录成功。

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

997209199418540d735089a0a2483466.png

2.1 登录与注销

  1. // 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
  2. StpUtil.login(Object id);

只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:

  • 检查此账号是否之前已有登录

  • 为账号生成 Token 凭证与 Session 会话

  • 通知全局侦听器,xx 账号登录成功

  • 将 Token 注入到请求上下文

  • 等等其它工作……

只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端。

此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。严格来讲是需要的,只不过 StpUtil.login(id) 方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。

  1. // 当前会话注销登录
  2. StpUtil.logout();
  3. // 获取当前会话是否已经登录,返回true=已登录,false=未登录
  4. StpUtil.isLogin();
  5. // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
  6. StpUtil.checkLogin();

2.2 会话查询

  1. // 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
  2. StpUtil.getLoginId();
  3. // 类似查询API还有:
  4. StpUtil.getLoginIdAsString();    // 获取当前会话账号id, 并转化为`String`类型
  5. StpUtil.getLoginIdAsInt();       // 获取当前会话账号id, 并转化为`int`类型
  6. StpUtil.getLoginIdAsLong();      // 获取当前会话账号id, 并转化为`long`类型
  7. // ---------- 指定未登录情形下返回的默认值 ----------
  8. // 获取当前会话账号id, 如果未登录,则返回null 
  9. StpUtil.getLoginIdDefaultNull();
  10. // 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
  11. StpUtil.getLoginId(T defaultValue);

2.3 Token 查询

  1. // 获取当前会话的token值
  2. StpUtil.getTokenValue();
  3. // 获取当前`StpLogic`的token名称
  4. StpUtil.getTokenName();
  5. // 获取指定token对应的账号id,如果未登录,则返回 null
  6. StpUtil.getLoginIdByToken(String tokenValue);
  7. // 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
  8. StpUtil.getTokenTimeout();
  9. // 获取当前会话的token信息参数
  10. StpUtil.getTokenInfo();

3. 权限认证

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

  • 有,就让你通过。

  • 没有?那么禁止访问!

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

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

2eafd5c9b91a261ef93e52eafdc0bc8d.png

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

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

你需要做的就是新建一个类,实现 StpInterface接口,例如以下代码:

  1. /**
  2.  * 自定义权限验证接口扩展
  3.  */
  4. @Component    // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展 
  5. public class StpInterfaceImpl implements StpInterface {
  6.     /**
  7.      * 返回一个账号所拥有的权限码集合 
  8.      */
  9.     @Override
  10.     public List<String> getPermissionList(Object loginId, String loginType) {
  11.         // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
  12.         List<String> list = new ArrayList<String>();    
  13.         list.add("101");
  14.         list.add("user.add");
  15.         list.add("user.update");
  16.         list.add("user.get");
  17.         // list.add("user.delete");
  18.         list.add("art.*");
  19.         return list;
  20.     }
  21.     /**
  22.      * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
  23.      */
  24.     @Override
  25.     public List<String> getRoleList(Object loginId, String loginType) {
  26.         // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
  27.         List<String> list = new ArrayList<String>();    
  28.         list.add("admin");
  29.         list.add("super-admin");
  30.         return list;
  31.     }
  32. }

参数解释:

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

  • loginType: 账号体系标识,此处暂时忽略,可以详细了解 [ 多账户认证 ]

3.2 权限校验

然后就可以用以下api来鉴权

  1. // 获取:当前账号所拥有的权限集合
  2. StpUtil.getPermissionList();
  3. // 判断:当前账号是否含有指定权限, 返回 true 或 false
  4. StpUtil.hasPermission("user.add");        
  5. // 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException 
  6. StpUtil.checkPermission("user.add");        
  7. // 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
  8. StpUtil.checkPermissionAnd("user.add""user.delete""user.get");        
  9. // 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
  10. StpUtil.checkPermissionOr("user.add""user.delete""user.get");

3.3 角色校验

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

  1. // 获取:当前账号所拥有的角色集合
  2. StpUtil.getRoleList();
  3. // 判断:当前账号是否拥有指定角色, 返回 true 或 false
  4. StpUtil.hasRole("super-admin");        
  5. // 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
  6. StpUtil.checkRole("super-admin");        
  7. // 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
  8. StpUtil.checkRoleAnd("super-admin""shop-admin");        
  9. // 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] 
  10. StpUtil.checkRoleOr("super-admin""shop-admin");
权限通配符

Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有art.*的权限时,art.addart.deleteart.update都将匹配通过

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

前端有了鉴权后端还需要鉴权吗?

需要!

前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全,无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验!

4. 前后台分离(无Cookie模式)

1.何为无 Cookie 模式?

无 Cookie 模式:特指不支持 Cookie 功能的终端,通俗来讲就是我们常说的 —— 前后台分离模式。

后端将 token 返回到前端

首先调用 StpUtil.login(id) 进行登录。

调用 StpUtil.getTokenInfo() 返回当前会话的 token 详细参数。

  • 此方法返回一个对象,其有两个关键属性:tokenNametokenValue(token 的名称和 token 的值)。

  • 将此对象传递到前台,让前端人员将这两个值保存到本地。

2.前端将 token 提交到后端

无论是app还是小程序,其传递方式都大同小异。

那就是,将 token 塞到请求header里 ,格式为:{tokenName: tokenValue}

3.只要按照如此方法将token值传递到后端,Sa-Token 就能像传统PC端一样自动读取到 token 值,进行鉴权。

4.你可能会有疑问,难道我每个ajax都要写这么一坨?岂不是麻烦死了?

你当然不能每个 ajax 都写这么一坨,因为这种重复性代码都是要封装在一个函数里统一调用的。

5. Sa-Token 集成 Redis

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

  • 重启后数据会丢失。

  • 无法在分布式环境中共享数据。

为此,Sa-Token 提供了扩展接口,你可以轻松将会话数据存储在 Redis、Memcached等专业的缓存中间件中, 做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。

以下是官方提供的 Redis 集成包:

  1. <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
  2. <dependency>
  3.     <groupId>cn.dev33</groupId>
  4.     <artifactId>sa-token-dao-redis-jackson</artifactId>
  5.     <version>1.34.0</version>
  6. </dependency>

集成 Redis 请注意:

1.无论使用哪种序列化方式,你都必须为项目提供一个 Redis 实例化方案,例如:

  1. <!-- 提供Redis连接池 -->
  2. <dependency>
  3.     <groupId>org.apache.commons</groupId>
  4.     <artifactId>commons-pool2</artifactId>
  5. </dependency>

2.配置Redis 连接信息

  1. spring: 
  2.     # redis配置 
  3.     redis:
  4.         # Redis数据库索引(默认为0
  5.         database: 0
  6.         # Redis服务器地址
  7.         host: 127.0.0.1
  8.         # Redis服务器连接端口
  9.         port: 6379
  10.         # Redis服务器连接密码(默认为空)
  11.         # password: 
  12.         # 连接超时时间
  13.         timeout: 10s
  14.         lettuce:
  15.             pool:
  16.                 # 连接池最大连接数
  17.                 max-active: 200
  18.                 # 连接池最大阻塞等待时间(使用负值表示没有限制)
  19.                 max-wait: -1ms
  20.                 # 连接池中的最大空闲连接
  21.                 max-idle: 10
  22.                 # 连接池中的最小空闲连接
  23.                 min-idle: 0

3.集成 Redis 后,是我额外手动保存数据,还是框架自动保存?

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

4.集成包版本问题

Sa-Token-Redis 集成包的版本尽量与 Sa-Token-Starter 集成包的版本一致,否则可能出现兼容性问题。

6. SpringBoot 集成 Sa-Token

以最新版本 1.34.0为例

6.1 创建项目

在 IDE 中新建一个 SpringBoot 项目,例如:sa-token-demo-springboot

6.2 添加依赖

在项目中添加依赖

  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
  6. <dependency>
  7.     <groupId>cn.dev33</groupId>
  8.     <artifactId>sa-token-spring-boot-starter</artifactId>
  9.     <version>1.34.0</version>
  10. </dependency>
  11. <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
  12. <dependency>
  13.     <groupId>cn.dev33</groupId>
  14.     <artifactId>sa-token-dao-redis-jackson</artifactId>
  15.     <version>1.34.0</version>
  16. </dependency>
  17. <!-- 提供Redis连接池 -->
  18. <dependency>
  19.     <groupId>org.apache.commons</groupId>
  20.     <artifactId>commons-pool2</artifactId>
  21. </dependency>
  22. <dependency>
  23.     <groupId>org.projectlombok</groupId>
  24.     <artifactId>lombok</artifactId>
  25. </dependency>

6.3 设置配置文件

  1. server:
  2.   # 端口
  3.   port: 8081
  4. ############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
  5. sa-token:
  6.   # token名称 (同时也是cookie名称)
  7.   token-name: satoken
  8.   # token有效期,单位s 默认30天, -1代表永不过期
  9.   timeout: 2592000
  10.   # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  11.   activity-timeout: -1
  12.   # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  13.   is-concurrent: true
  14.   # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  15.   is-share: true
  16.   # token风格
  17.   token-style: uuid
  18.   # 是否输出操作日志
  19.   is-log: false
  20. spring:
  21.   # redis配置
  22.   redis:
  23.     # Redis数据库索引(默认为0
  24.     database: 0
  25.     # Redis服务器地址
  26.     host: 127.0.0.1
  27.     # Redis服务器连接端口
  28.     port: 6379
  29.     # Redis服务器连接密码(默认为空)
  30.     password: abc123
  31.     # 连接超时时间
  32.     timeout: 10s
  33.     lettuce:
  34.       pool:
  35.         # 连接池最大连接数
  36.         max-active: 200
  37.         # 连接池最大阻塞等待时间(使用负值表示没有限制)
  38.         max-wait: -1ms
  39.         # 连接池中的最大空闲连接
  40.         max-idle: 10
  41.         # 连接池中的最小空闲连接
  42.         min-idle: 0

6.4 创建启动类

  1. @SpringBootApplication
  2. public class SaTokenDemoApplication {
  3.     public static void main(String[] args) throws JsonProcessingException {
  4.         SpringApplication.run(SaTokenDemoApplication.class, args);
  5.         System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig());
  6.     }
  7. }

6.5 定义用户信息类

  1. @Data
  2. @Accessors(chain = true)
  3. @AllArgsConstructor
  4. public class UserInfo {
  5.     private Long userId;
  6.     private String name;
  7.     private String email;
  8. }

6.6 自定义权限验证接口扩展

  1. @Component    // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展
  2. public class StpInterfaceImpl implements StpInterface {
  3.     /**
  4.      * 返回一个账号所拥有的权限码集合
  5.      */
  6.     @Override
  7.     public List<String> getPermissionList(Object loginId, String loginType) {
  8.         // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
  9. //        List<String> list = new ArrayList<String>();
  10. //        list.add("user.add");
  11. //        list.add("user.get");
  12.         // list.add("user.delete");
  13. //        list.add("art.*");
  14.         //从redis中获取权限
  15.         List<String> authList = (List<String>) StpUtil.getSession().get("authList");
  16.         return authList;
  17.     }
  18.     /**
  19.      * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
  20.      * @param loginId 账号id,即你在调用 StpUtil.login(id) 时写入的标识值
  21.      * @param loginType 账号体系标识
  22.      * @author: yh
  23.      * @date: 2023/2/12
  24.      */
  25.     @Override
  26.     public List<String> getRoleList(Object loginId, String loginType) {
  27.         // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
  28.         List<String> list = new ArrayList<String>();
  29.         list.add("admin");
  30.         list.add("super-admin");
  31.         return list;
  32.     }
  33. }

6.7 创建测试Controller

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4.     /**
  5.      * 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456
  6.      */
  7.     @RequestMapping("/doLogin")
  8.     public SaResult doLogin(String username, String password) {
  9.         // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
  10.         if("zhang".equals(username) && "123456".equals(password)) {
  11.             // 第1步,先登录上
  12.             StpUtil.login(10001);
  13.         }
  14.         //第2步,加载用户信息和权限信息
  15.         StpUtil.getSession().set("loginInfo"new UserInfo(10001L, "张三""123123@163.com"));
  16.         //加载权限,只给user.add的权限
  17.         List<String> authList = new ArrayList<String>();
  18.         authList.add("user.add");
  19.         StpUtil.getSession().set("authList", authList);
  20.         // 第3步,获取 Token  相关参数
  21.         SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
  22.         // 第4步,返回给前端
  23.         return SaResult.data(tokenInfo);
  24.     }
  25.     /**
  26.      * 查询登录状态
  27.      */
  28.     @RequestMapping("/isLogin")
  29.     public String isLogin() {
  30.         return "当前会话是否登录:" + StpUtil.isLogin();
  31.     }
  32.     /**
  33.      * 获取用户信息
  34.      */
  35.     @RequestMapping("/getUserInfo")
  36.     public UserInfo getUserInfo() {
  37.         UserInfo loginInfo = (UserInfo) StpUtil.getSession().get("loginInfo");
  38.         return loginInfo;
  39.     }
  40.     /**
  41.      * 测试方法校验权限
  42.      */
  43.     @GetMapping(value = "/add")
  44.     public String add(){
  45.         StpUtil.checkPermission("user.add");
  46.         return "ok";
  47.     }
  48.     @GetMapping(value = "/update")
  49.     public String update(){
  50.         StpUtil.checkPermission("user.update");
  51.         return "ok";
  52.     }
  53. }

6.8 运行

启动项目,Copy Configuration再启动一个实例,这时候我们就同时启动了两个实例

f970c3b17c109a6318fbd38419f4cf62.png 09088f652b390381eb876aefe00bf4cd.png

这样就可以测试出在集群部署情况下登录信息会不会有问题,然后用接口测试工具依次访问上述测试接口

1、 登录
  • http://localhost:8081/user/doLogin?username=zhang&password=123456

72d11ec560ba4e29a307274203aea5cd.png

此时查看redis中数据,可以看到登录信息和权限都保存在redis中了

551ed5c904f1455a9b85bdc43afb32e5.png
2、 查询登录状态
  • http://localhost:8081/user/isLogin

注意header里没有cookie

defe39e7d0bb864ca256075df1c27b87.png

把登录接口返回的tokenName和tokenValue加入到请求header中

f1f76ebb97307acd1e050a9143018b84.png

只有请求携带对应的token,登录状态才为:true

3、 获取用户信息

这里调用端口为8082的实例

  • http://localhost:8082/user/getUserInfo

f93995e8a2c4e732c4e30cc7aec900f6.png
4、 调用添加用户接口

测试是否有添加接口权限,登录的时候我们赋予了添加用户的权限

  • http://localhost:8082/user/add

4087501851ca3d1612c3ac1a89e94848.png
5、 调用更新用接口

测试是否有更新用户接口权限,登录的时候我们没有赋予更新用户的权限

  • http://localhost:8082/user/update

d0ba51d77d3018673010aefa5a8a65e2.png 05a7614fe86a7c1e640e35cbedf4d575.png

结果可以看到没有更新用户接口的权限。

感谢阅读,希望对你有所帮助 :)   来源:

blog.csdn.net/weixin_43847283/article/details/128995172

  1. 推荐:
  2. 最全的java面试题库
  3. PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/933748
推荐阅读
相关标签
  

闽ICP备14008679号