SpringBoot 项目使用 Sa-Token 完成登录认证

打印 上一主题 下一主题

主题 575|帖子 575|积分 1725

一、设计思路

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

  • 如果校验通过,则:正常返回数据。
  • 如果校验未通过,则:抛出异常,告知其需要先进行登录。
那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:

  • 用户提交 name + password 参数,调用登录接口。
  • 登录成功,返回这个用户的 Token 会话凭证。
  • 用户后续的每次请求,都携带上这个 Token。
  • 服务器根据 Token 判断此会话是否登录成功。
所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。
动态图演示:

接下来,我们将介绍在 SpringBoot 中如何使用 Sa-Token 完成登录认证操作。
Sa-Token 是一个 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
Gitee 开源地址:https://gitee.com/dromara/sa-token
首先在项目中引入 Sa-Token 依赖:
  1. <dependency>
  2.     <groupId>cn.dev33</groupId>
  3.     <artifactId>sa-token-spring-boot-starter</artifactId>
  4.     <version>1.34.0</version>
  5. </dependency>
复制代码
注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。
二、登录与注销

根据以上思路,我们需要一个会话登录的函数:
  1. // 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
  2. StpUtil.login(Object id);         
复制代码
只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:

  • 检查此账号是否之前已有登录
  • 为账号生成 Token 凭证与 Session 会话
  • 通知全局侦听器,xx 账号登录成功
  • 将 Token 注入到请求上下文
  • 等等其它工作……
你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端。
所以一般情况下,我们的登录接口代码,会大致类似如下:
  1. // 会话登录接口
  2. @RequestMapping("doLogin")
  3. public SaResult doLogin(String name, String pwd) {
  4.         // 第一步:比对前端提交的账号名称、密码
  5.         if("zhang".equals(name) && "123456".equals(pwd)) {
  6.                 // 第二步:根据账号id,进行登录
  7.                 StpUtil.login(10001);
  8.                 return SaResult.ok("登录成功");
  9.         }
  10.         return SaResult.error("登录失败");
  11. }
复制代码
如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。
是因为不需要吗?严格来讲是需要的,只不过 StpUtil.login(id) 方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。
如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点:

  • Cookie 可以从后端控制往浏览器中写入 Token 值。
  • Cookie 会在前端每次发起请求时自动提交 Token 值。
因此,在 Cookie 功能的加持下,我们可以仅靠 StpUtil.login(id) 一句代码就完成登录认证。
除了登录方法,我们还需要:
  1. // 当前会话注销登录
  2. StpUtil.logout();
  3. // 获取当前会话是否已经登录,返回true=已登录,false=未登录
  4. StpUtil.isLogin();
  5. // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
  6. StpUtil.checkLogin();
复制代码
异常 NotLoginException 代表当前会话暂未登录,可能的原因有很多:
前端没有提交 Token、前端提交的 Token 是无效的、前端提交的 Token 已经过期 …… 等等。
Sa-Token 未登录场景值参照表:
场景值对应常量含义说明-1NotLoginException.NOT_TOKEN未能从请求中读取到 Token-2NotLoginException.INVALID_TOKEN已读取到 Token,但是 Token无效-3NotLoginException.TOKEN_TIMEOUT已读取到 Token,但是 Token已经过期-4NotLoginException.BE_REPLACED已读取到 Token,但是 Token 已被顶下线-5NotLoginException.KICK_OUT已读取到 Token,但是 Token 已被踢下线那么,如何获取场景值呢?废话少说直接上代码:
  1. // 全局异常拦截(拦截项目中的NotLoginException异常)
  2. @ExceptionHandler(NotLoginException.class)
  3. public SaResult handlerNotLoginException(NotLoginException nle)
  4.                 throws Exception {
  5.         // 打印堆栈,以供调试
  6.         nle.printStackTrace();
  7.        
  8.         // 判断场景值,定制化异常信息
  9.         String message = "";
  10.         if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
  11.                 message = "未提供token";
  12.         }
  13.         else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
  14.                 message = "token无效";
  15.         }
  16.         else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
  17.                 message = "token已过期";
  18.         }
  19.         else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
  20.                 message = "token已被顶下线";
  21.         }
  22.         else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
  23.                 message = "token已被踢下线";
  24.         }
  25.         else {
  26.                 message = "当前会话未登录";
  27.         }
  28.        
  29.         // 返回给前端
  30.         return SaResult.error(message);
  31. }
复制代码
注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理三、会话查询
  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);
复制代码
四、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();
复制代码
TokenInfo 是 Token 信息 Model,用来描述一个 Token 的常用参数:
  1. {
  2.         "tokenName": "satoken",           // token名称
  3.         "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633",      // token值
  4.         "isLogin": true,                  // 此token是否已经登录
  5.         "loginId": "10001",               // 此token对应的LoginId,未登录时为null
  6.         "loginType": "login",              // 账号类型标识
  7.         "tokenTimeout": 2591977,          // token剩余有效期 (单位: 秒)
  8.         "sessionTimeout": 2591977,        // User-Session剩余有效时间 (单位: 秒)
  9.         "tokenSessionTimeout": -2,        // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
  10.         "tokenActivityTimeout": -1,       // token剩余无操作有效时间 (单位: 秒)
  11.         "loginDevice": "default-device"   // 登录设备类型
  12. }
复制代码
五、来个小测试,加深一下理解

新建 LoginAuthController,复制以下代码
  1. package com.pj.cases.use;
  2. import org.springframework.web.bind.annotation.RequestMapping;
  3. import org.springframework.web.bind.annotation.RestController;
  4. import cn.dev33.satoken.stp.SaTokenInfo;
  5. import cn.dev33.satoken.stp.StpUtil;
  6. import cn.dev33.satoken.util.SaResult;
  7. /**
  8. * Sa-Token 登录认证示例
  9. *
  10. * @author kong
  11. * @since 2022-10-13
  12. */
  13. @RestController
  14. @RequestMapping("/acc/")
  15. public class LoginAuthController {
  16.         // 会话登录接口  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
  17.         @RequestMapping("doLogin")
  18.         public SaResult doLogin(String name, String pwd) {
  19.                
  20.                 // 第一步:比对前端提交的 账号名称 & 密码 是否正确,比对成功后开始登录
  21.                 //                 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
  22.                 if("zhang".equals(name) && "123456".equals(pwd)) {
  23.                        
  24.                         // 第二步:根据账号id,进行登录
  25.                         //                 此处填入的参数应该保持用户表唯一,比如用户id,不可以直接填入整个 User 对象
  26.                         StpUtil.login(10001);
  27.                        
  28.                         // SaResult 是 Sa-Token 中对返回结果的简单封装,下面的示例将不再赘述
  29.                         return SaResult.ok("登录成功");
  30.                 }
  31.                
  32.                 return SaResult.error("登录失败");
  33.         }
  34.         // 查询当前登录状态  ---- http://localhost:8081/acc/isLogin
  35.         @RequestMapping("isLogin")
  36.         public SaResult isLogin() {
  37.                 // StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false
  38.                 boolean isLogin = StpUtil.isLogin();
  39.                 return SaResult.ok("当前客户端是否登录:" + isLogin);
  40.         }
  41.         // 校验当前登录状态  ---- http://localhost:8081/acc/checkLogin
  42.         @RequestMapping("checkLogin")
  43.         public SaResult checkLogin() {
  44.                 // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
  45.                 StpUtil.checkLogin();
  46.                 // 抛出异常后,代码将走入全局异常处理(GlobalException.java),如果没有抛出异常,则代表通过了登录校验,返回下面信息
  47.                 return SaResult.ok("校验登录成功,这行字符串是只有登录后才会返回的信息");
  48.         }
  49.         // 获取当前登录的账号是谁  ---- http://localhost:8081/acc/getLoginId
  50.         @RequestMapping("getLoginId")
  51.         public SaResult getLoginId() {
  52.                 // 需要注意的是,StpUtil.getLoginId() 自带登录校验效果
  53.                 // 也就是说如果在未登录的情况下调用这句代码,框架就会抛出 `NotLoginException` 异常,效果和 StpUtil.checkLogin() 是一样的
  54.                 Object userId = StpUtil.getLoginId();
  55.                 System.out.println("当前登录的账号id是:" + userId);
  56.                
  57.                 // 如果不希望 StpUtil.getLoginId() 触发登录校验效果,可以填入一个默认值
  58.                 // 如果会话未登录,则返回这个默认值,如果会话已登录,将正常返回登录的账号id
  59.                 Object userId2 = StpUtil.getLoginId(0);
  60.                 System.out.println("当前登录的账号id是:" + userId2);
  61.                
  62.                 // 或者使其在未登录的时候返回 null
  63.                 Object userId3 = StpUtil.getLoginIdDefaultNull();
  64.                 System.out.println("当前登录的账号id是:" + userId3);
  65.                
  66.                 // 类型转换:
  67.                 // StpUtil.getLoginId() 返回的是 Object 类型,你可以使用以下方法指定其返回的类型
  68.                 int userId4 = StpUtil.getLoginIdAsInt();  // 将返回值转换为 int 类型
  69.                 long userId5 = StpUtil.getLoginIdAsLong();  // 将返回值转换为 long 类型
  70.                 String userId6 = StpUtil.getLoginIdAsString();  // 将返回值转换为 String 类型
  71.                
  72.                 // 疑问:数据基本类型不是有八个吗,为什么只封装以上三种类型的转换?
  73.                 // 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的,实在没见过哪个项目用 double、float、boolean 之类来声明 UserId
  74.                 System.out.println("当前登录的账号id是:" + userId4 + " --- " + userId5 + " --- " + userId6);
  75.                
  76.                 // 返回给前端
  77.                 return SaResult.ok("当前客户端登录的账号id是:" + userId);
  78.         }
  79.         // 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo
  80.         @RequestMapping("tokenInfo")
  81.         public SaResult tokenInfo() {
  82.                 // TokenName 是 Token 名称的意思,此值也决定了前端提交 Token 时应该使用的参数名称
  83.                 String tokenName = StpUtil.getTokenName();
  84.                 System.out.println("前端提交 Token 时应该使用的参数名称:" + tokenName);
  85.                
  86.                 // 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值
  87.                 // 框架默认前端可以从以下三个途径中提交 Token:
  88.                 //                 Cookie                 (浏览器自动提交)
  89.                 //                 Header头        (代码手动提交)
  90.                 //                 Query 参数        (代码手动提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx
  91.                 // 读取顺序为: Query 参数 --> Header头 -- > Cookie
  92.                 // 以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token
  93.                 String tokenValue = StpUtil.getTokenValue();
  94.                 System.out.println("前端提交的Token值为:" + tokenValue);
  95.                
  96.                 // TokenInfo 包含了此 Token 的大多数信息
  97.                 SaTokenInfo info = StpUtil.getTokenInfo();
  98.                 System.out.println("Token 名称:" + info.getTokenName());
  99.                 System.out.println("Token 值:" + info.getTokenValue());
  100.                 System.out.println("当前是否登录:" + info.getIsLogin());
  101.                 System.out.println("当前登录的账号id:" + info.getLoginId());
  102.                 System.out.println("当前登录账号的类型:" + info.getLoginType());
  103.                 System.out.println("当前登录客户端的设备类型:" + info.getLoginDevice());
  104.                 System.out.println("当前 Token 的剩余有效期:" + info.getTokenTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
  105.                 System.out.println("当前 Token 的剩余临时有效期:" + info.getTokenActivityTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
  106.                 System.out.println("当前 User-Session 的剩余有效期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
  107.                 System.out.println("当前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
  108.                
  109.                 // 返回给前端
  110.                 return SaResult.data(StpUtil.getTokenInfo());
  111.         }
  112.        
  113.         // 会话注销  ---- http://localhost:8081/acc/logout
  114.         @RequestMapping("logout")
  115.         public SaResult logout() {
  116.                 // 退出登录会清除三个地方的数据:
  117.                 //                 1、Redis中保存的 Token 信息
  118.                 //                 2、当前请求上下文中保存的 Token 信息
  119.                 //                 3、Cookie 中保存的 Token 信息(如果未使用Cookie模式则不会清除)
  120.                 StpUtil.logout();
  121.                
  122.                 // StpUtil.logout() 在未登录时也是可以调用成功的,
  123.                 // 也就是说,无论客户端有没有登录,执行完 StpUtil.logout() 后,都会处于未登录状态
  124.                 System.out.println("当前是否处于登录状态:" + StpUtil.isLogin());
  125.                
  126.                 // 返回给前端
  127.                 return SaResult.ok("退出登录成功");
  128.         }
  129.        
  130. }
复制代码
代码注释已针对每一步操作做出详细解释,大家可根据可参照注释中的访问链接进行逐步测试。
本示例代码已上传至 Gitee,可参考:
Sa-Token 登录认证示例
参考资料


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

道家人

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表