一、设计思路

关于一些登录之后才能拜访的接口(例如:查询我的账号资料),咱们一般的做法是添加一层接口校验:

  • 假如校验经过,则:正常回来数据。
  • 假如校验未经过,则:抛出反常,奉告其需求先进行登录。

那么,判别会话是否登录的依据是什么?咱们先来简略分析一下登录拜访流程:

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

所谓登录认证,指的便是服务器校验账号密码,为用户颁布 Token 会话凭据的进程,这个 Token 也是咱们后续判别会话是否登录的关键所在。

动态图演示:

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

接下来,咱们将介绍在 SpringBoot 中怎么运用 Sa-Token 完结登录认证操作。

Sa-Token 是一个 java 权限认证结构,首要处理登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。 Gitee 开源地址:gitee.com/dromara/sa-…

首先在项目中引进 Sa-Token 依靠:

<!-- Sa-Token 权限认证 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.34.0</version>
</dependency>

注:假如你运用的是 SpringBoot 3.x,只需求将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

二、登录与注销

依据以上思路,咱们需求一个会话登录的函数:

// 会话登录:参数填写要登录的账号id,主张的数据类型:long | int | String, 不能够传入杂乱类型,如:User、Admin 等等
StpUtil.login(Object id);	 

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

  1. 查看此账号是否之前已有登录
  2. 为账号生成 Token 凭据与 Session 会话
  3. 告诉大局侦听器,xx 账号登录成功
  4. Token 注入到恳求上下文
  5. 等等其它作业……

你暂时不需求完整的了解整个登录进程,你只需求记住关键一点:Sa-Token 为这个账号创建了一个Token凭据,且经过 Cookie 上下文回来给了前端

所以一般情况下,咱们的登录接口代码,会大致相似如下:

// 会话登录接口 
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
	// 第一步:比对前端提交的账号称号、密码
	if("zhang".equals(name) && "123456".equals(pwd)) {
		// 第二步:依据账号id,进行登录 
		StpUtil.login(10001);
		return SaResult.ok("登录成功");
	}
	return SaResult.error("登录失利");
}

假如你对以上代码阅览没有压力,你可能会留意到略显奇怪的一点:此处只是做了会话登录,但并没有主意向前端回来 Token 信息。 是因为不需求吗?严厉来讲是需求的,只不过 StpUtil.login(id) 办法利用了 Cookie 主动注入的特性,省略了你手写回来 Token 的代码。

假如你对 Cookie 功能还不太了解,也不用担心,咱们会在之后的 [ 前后端别离 ] 章节中具体的论述 Cookie 功能,现在你只需求了解最基本的两点:

  • Cookie 能够从后端控制往浏览器中写入 Token 值。
  • Cookie 会在前端每次发起恳求时主动提交 Token 值。

因而,在 Cookie 功能的加持下,咱们能够仅靠 StpUtil.login(id) 一句代码就完结登录认证。

除了登录办法,咱们还需求:

// 当时会话注销登录
StpUtil.logout();
// 获取当时会话是否现已登录,回来true=已登录,false=未登录
StpUtil.isLogin();
// 查验当时会话是否现已登录, 假如未登录,则抛出反常:`NotLoginException`
StpUtil.checkLogin();

反常 NotLoginException 代表当时会话暂未登录,可能的原因有许多: 前端没有提交 Token、前端提交的 Token 是无效的、前端提交的 Token 现已过期 …… 等等。

Sa-Token 未登录场景值参照表:

场景值 对应常量 意义说明
-1 NotLoginException.NOT_TOKEN 未能从恳求中读取到 Token
-2 NotLoginException.INVALID_TOKEN 已读取到 Token,可是 Token无效
-3 NotLoginException.TOKEN_TIMEOUT 已读取到 Token,可是 Token现已过期
-4 NotLoginException.BE_REPLACED 已读取到 Token,可是 Token 已被顶下线
-5 NotLoginException.KICK_OUT 已读取到 Token,可是 Token 已被踢下线

那么,怎么获取场景值呢?废话少说直接上代码:

// 大局反常阻拦(阻拦项目中的NotLoginException反常)
@ExceptionHandler(NotLoginException.class)
public SaResult handlerNotLoginException(NotLoginException nle)
		throws Exception {
	// 打印库房,以供调试
	nle.printStackTrace(); 
	// 判别场景值,定制化反常信息 
	String message = "";
	if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
		message = "未提供token";
	}
	else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
		message = "token无效";
	}
	else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
		message = "token已过期";
	}
	else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
		message = "token已被顶下线";
	}
	else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
		message = "token已被踢下线";
	}
	else {
		message = "当时会话未登录";
	}
	// 回来给前端
	return SaResult.error(message);
}

留意:以上代码并非处理逻辑的最佳方式,只为以最简略的代码演示出场景值的获取与运用,我们能够依据自己的项目需求来定制化处理

三、会话查询

// 获取当时会话账号id, 假如未登录,则抛出反常:`NotLoginException`
StpUtil.getLoginId();
// 相似查询API还有:
StpUtil.getLoginIdAsString();    // 获取当时会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt();       // 获取当时会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong();      // 获取当时会话账号id, 并转化为`long`类型
// ---------- 指定未登录景象下回来的默认值 ----------
// 获取当时会话账号id, 假如未登录,则回来null 
StpUtil.getLoginIdDefaultNull();
// 获取当时会话账号id, 假如未登录,则回来默认值 (`defaultValue`能够为恣意类型)
StpUtil.getLoginId(T defaultValue);

四、Token 查询

// 获取当时会话的token值
StpUtil.getTokenValue();
// 获取当时`StpLogic`的token称号
StpUtil.getTokenName();
// 获取指定token对应的账号id,假如未登录,则回来 null
StpUtil.getLoginIdByToken(String tokenValue);
// 获取当时会话剩下有用期(单位:s,回来-1代表永久有用)
StpUtil.getTokenTimeout();
// 获取当时会话的token信息参数
StpUtil.getTokenInfo();

TokenInfo 是 Token 信息 Model,用来描绘一个 Token 的常用参数:

{
	"tokenName": "satoken",           // token称号
	"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633",      // token值
	"isLogin": true,                  // 此token是否现已登录
	"loginId": "10001",               // 此token对应的LoginId,未登录时为null
	"loginType": "login",              // 账号类型标识
	"tokenTimeout": 2591977,          // token剩下有用期 (单位: 秒)
	"sessionTimeout": 2591977,        // User-Session剩下有用时刻 (单位: 秒)
	"tokenSessionTimeout": -2,        // Token-Session剩下有用时刻 (单位: 秒) (-2表示系统中不存在这个缓存)
	"tokenActivityTimeout": -1,       // token剩下无操作有用时刻 (单位: 秒)
	"loginDevice": "default-device"   // 登录设备类型 
}

五、来个小测验,加深一下了解

新建 LoginAuthController,复制以下代码

package com.pj.cases.use;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
/**
 * Sa-Token 登录认证示例 
 * 
 * @author kong
 * @since 2022-10-13
 */
@RestController
@RequestMapping("/acc/")
public class LoginAuthController {
	// 会话登录接口  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
	@RequestMapping("doLogin")
	public SaResult doLogin(String name, String pwd) {
		// 第一步:比对前端提交的 账号称号 & 密码 是否正确,比对成功后开端登录 
		// 		此处仅作模仿示例,真实项目需求从数据库中查询数据进行比对 
		if("zhang".equals(name) && "123456".equals(pwd)) {
			// 第二步:依据账号id,进行登录 
			// 		此处填入的参数应该坚持用户表唯一,比如用户id,不能够直接填入整个 User 目标 
			StpUtil.login(10001);
			// SaResult 是 Sa-Token 中对回来结果的简略封装,下面的示例将不再赘述 
			return SaResult.ok("登录成功");
		}
		return SaResult.error("登录失利");
	}
	// 查询当时登录状况  ---- http://localhost:8081/acc/isLogin
	@RequestMapping("isLogin")
	public SaResult isLogin() {
		// StpUtil.isLogin() 查询当时客户端是否登录,回来 true 或 false 
		boolean isLogin = StpUtil.isLogin();
		return SaResult.ok("当时客户端是否登录:" + isLogin);
	}
	// 校验当时登录状况  ---- http://localhost:8081/acc/checkLogin
	@RequestMapping("checkLogin")
	public SaResult checkLogin() {
		// 查验当时会话是否现已登录, 假如未登录,则抛出反常:`NotLoginException`
		StpUtil.checkLogin();
		// 抛出反常后,代码将走入大局反常处理(GlobalException.java),假如没有抛出反常,则代表经过了登录校验,回来下面信息 
		return SaResult.ok("校验登录成功,这行字符串是只要登录后才会回来的信息");
	}
	// 获取当时登录的账号是谁  ---- http://localhost:8081/acc/getLoginId
	@RequestMapping("getLoginId")
	public SaResult getLoginId() {
		// 需求留意的是,StpUtil.getLoginId() 自带登录校验作用
		// 也便是说假如在未登录的情况下调用这句代码,结构就会抛出 `NotLoginException` 反常,作用和 StpUtil.checkLogin() 是相同的 
		Object userId = StpUtil.getLoginId();
		System.out.println("当时登录的账号id是:" + userId);
		// 假如不希望 StpUtil.getLoginId() 触发登录校验作用,能够填入一个默认值
		// 假如会话未登录,则回来这个默认值,假如会话已登录,将正常回来登录的账号id 
		Object userId2 = StpUtil.getLoginId(0);
		System.out.println("当时登录的账号id是:" + userId2);
		// 或者使其在未登录的时分回来 null 
		Object userId3 = StpUtil.getLoginIdDefaultNull();
		System.out.println("当时登录的账号id是:" + userId3);
		// 类型转换:
		// StpUtil.getLoginId() 回来的是 Object 类型,你能够运用以下办法指定其回来的类型 
		int userId4 = StpUtil.getLoginIdAsInt();  // 将回来值转换为 int 类型 
		long userId5 = StpUtil.getLoginIdAsLong();  // 将回来值转换为 long 类型 
		String userId6 = StpUtil.getLoginIdAsString();  // 将回来值转换为 String 类型 
		// 疑问:数据基本类型不是有八个吗,为什么只封装以上三种类型的转换?
		// 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的,实在没见过哪个项目用 double、float、boolean 之类来声明 UserId 
		System.out.println("当时登录的账号id是:" + userId4 + " --- " + userId5 + " --- " + userId6);
		// 回来给前端 
		return SaResult.ok("当时客户端登录的账号id是:" + userId);
	}
	// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo
	@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.getTokenActivityTimeout()); // 单位:秒,-1代表永久有用,-2代表值不存在
		System.out.println("当时 User-Session 的剩下有用期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有用,-2代表值不存在
		System.out.println("当时 Token-Session 的剩下有用期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有用,-2代表值不存在
		// 回来给前端 
		return SaResult.data(StpUtil.getTokenInfo());
	}
	// 会话注销  ---- http://localhost:8081/acc/logout
	@RequestMapping("logout")
	public SaResult logout() {
		// 退出登录会清除三个当地的数据:
		// 		1、Redis中保存的 Token 信息
		// 		2、当时恳求上下文中保存的 Token 信息 
		// 		3、Cookie 中保存的 Token 信息(假如未运用Cookie形式则不会清除)
		StpUtil.logout();
		// StpUtil.logout() 在未登录时也是能够调用成功的,
		// 也便是说,不管客户端有没有登录,履行完 StpUtil.logout() 后,都会处于未登录状况 
		System.out.println("当时是否处于登录状况:" + StpUtil.isLogin());
		// 回来给前端 
		return SaResult.ok("退出登录成功");
	}
}

代码注释已针对每一步操作做出具体解释,我们可依据可参照注释中的拜访链接进行逐步测验。

本示例代码已上传至 Gitee,可参考: Sa-Token 登录认证示例


参考资料

  • Gitee 库房地址:gitee.com/dromara/sa-…
  • GitHub 库房地址:github.com/dromara/sa-…
  • Sa-Token 在线文档:sa-token.dev33.cn/