Sa-Token
约 7226 字大约 24 分钟
2025-04-11
1.Sa-token 全面概述
在业务开发的时候,新手第一个碰到的难题就是如何进行权限校验的问题,登录和注册功能一直都是困惑初入开发朋友的一道门槛,您需要熟悉 HTTP
文档,并且还需要应对各种问题。不过在有了 Sa-token
以后,这一切都将变得明朗。Sa-token
是轻量的 Java
权限认证框架,可以集成到 Spring Boot
进行快速的使用。您可以阅读 官方文档,这里更多是偏向实战。
2.Sa-token 基本功能
Sa-Token
目前主要五大功能模块:
- 登录认证
- 权限认证
- 用户下线
- 单点登录
- 三方登录
- 微服鉴权

3.Sa-token 使用教程
3.1.基础使用
3.1.1.登录认证
首先我们需要理解登录的本质是什么,一般而言实现登录逻辑主要依赖两种方案,Session
和 Token
两种方案,两种方案我们需要对比一下,假设我们有多个服务器和一个浏览器。
登录方案 | 运行原理 | 状态存储位置 | 安全性 | 横向扩展 | 适用场景 | 跨域支持 |
---|---|---|---|---|---|---|
Session | 用户发出登录请求后,服务器将校验登录凭证,然后生成一个 sessionId ,并且对应保存用户信息在内存中(一般使用 Redis 进行保存),然后响应一个 sessionId 返回给浏览器。而浏览器将存储这个 sessionId 到 cookie 里,后续所有的请求都需要携带 cookie ,而其他接口通过这个 cookie 值也就是 sessionId 来找回存储在内存中的 session 数据,然后根据内存中对应用户状态信息进行响应即可。 | 服务器 | 较高 | 较难,需要做分布式 Session 否则所有的服务无法同时共享用户的登录信息(比如最重要的 userId ) | 单服务系统 | 需要手动使用 withCredentials: true |
Token | 用户发出登录请求后,服务器间校验登录凭证,然后直接生成一个 token (通常使用 JWT 标准,这里面包含用户的信息和一些防篡改的机制),然后直接响应给浏览器存储,不需要在服务端内存中进行存储。浏览器存储这个 token 后,后续的请求需要直接携带 token ,由服务器检测这个 token 是否有效即可。 | 浏览器 | 较低 | 较易,每个服务都可以从 token 中读取到完整的用户信息(比如最重要的 userId ) | 分布式系统 | 无需手动使用 withCredentials: true |
重要
补充:关于跨域的情况,可以简单看看两种方案对应的 HTTP
报文,这样看是最清晰的!
如果是 Session
机制的情况下:
浏览器请求登录
POST /api/user/login HTTP/1.1 Host: api.example.com <- 这是需要访问的后端域名 Origin: https://web.example.org <- 这是当前请求的来源域名(此时域名不相同发生跨域) Content-Type: application/json { "username": "foo", "password": "bar" }
服务器响应登录
HTTP/1.1 200 OK <- 在响应之前会把用户登录凭证存储为后端内存中 session, 并且把表示该 session 的 sessionId 交给浏览器, 以备后续校验登录信息 Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=None <- 告诉浏览器为当前前端域名设置一个 cookie; HttpOnly 可以防止被 js 代码访问; Secure 表示限制使用 https 协议; SameSite=None 表示允许跨域携带 cookie 请求域名 api.example.com Access-Control-Allow-Origin: https://web.example.org <- 告诉浏览器后端域名允许这个前端域名向自己发起跨域 Access-Control-Allow-Credentials: true <- 告诉浏览器允许在前面的前端域名请求时发送凭证, 例如 cookie, 并且设置这个无法同时设置 Access-Control-Allow-Origin 为 *
浏览器再次请求
GET /api/user/info HTTP/1.1 Host: api.example.com <- 这是需要访问的后端域名 Origin: https://web.example.org <- 当前请求的来源域名 Cookie: sessionid=abc123 <- 浏览器自动带上 cookie(前提是前端代码开启 credentials: 'include', 否则默认跨域的情况下无法携带 Cookie 进行请求)
服务器再次响应
HTTP/1.1 200 OK Content-Type: application/json { "username": "foo", "email": "foo@example.com" }
如果是 Token
机制的情况下:
浏览器请求登录
POST /api/user/login HTTP/1.1 Host: api.example.com Origin: https://web.example.org Content-Type: application/json { "username": "foo", "password": "bar" }
服务器响应登录
HTTP/1.1 200 OK Content-Type: application/json Access-Control-Allow-Origin: https://web.example.org Access-Control-Allow-Credentials: true { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3ODkwLCJpYXQiOjE2MTYwMjI3NTZ9.2AbF3z7J_TpBOJ-bZJYr5Z8xtHKHjznxShuA2k9hy0I" }
浏览器再次请求
GET /api/user/info HTTP/1.1 Host: api.example.com Origin: https://web.example.org Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3ODkwLCJpYXQiOjE2MTYwMjI3NTZ9.2AbF3z7J_TpBOJ-bZJYr5Z8xtHKHjznxShuA2k9hy0I <- Bearer 是一种约定,表示后面跟着的就是 Token; 并且携带 token 不需要前端代码启动 withCredentials: true
服务器再次响应
HTTP/1.1 200 OK Content-Type: application/json { "username": "foo", "email": "foo@example.com" }
保存在浏览器的机制使得 Token
机制非常适合分布式应用,因为不再需要维护一个共享的 Session
仓库。
不过 Sa-token
还是基于 Session
的机制实现的(因为利用了 HTTP
报文中的 Cookie
机制),不过凭证也叫做 token
,并且机制会更加复杂一些,可以说融合了两者。不过,我们可以先尝试一下,您需要使用 Spring Boot
依赖和 Sa-token
的集成依赖,并且还需要配置一个配置文件,可以 查阅官方文档。这里我编写伪代码让您能理解,如果您好奇实战写法,则可以前往这个 work-user-center 项目中进行具体的用法(下面的代码就是基于这个项目中抽离出来的核心代码)。
<!-- Sa-Token -->
<dependency>
<!-- 权限认证,在线文档:https://sa-token.cc -->
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId> <!-- 注意我使用的是 Spring Boot3 -->
<version>1.42.0</version>
</dependency>
# sa-token 配置
# sa-token 配置
sa-token:
## token 名称
token-name: work-user-centre # 同时也是 cookie 名称
## token 有效期
timeout: 2592000 # 单位为秒, 默认 30 天, -1 代表永久有效
## token 最低活跃频率
active-timeout: -1 # 单位:为秒, 如果 token 超过此时间没有访问系统就会被冻结, 默认 -1 代表不限制, 永不冻结
## token 共享
is-share: false # 在多人登录同一账号时, 是否共用一个 token(为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
## 是否允许同一账号多地同时登录
is-concurrent: true # 为 true 时允许一起登录, 为 false 时新登录挤掉旧登录
## token 风格
token-style: uuid # 默认可取值: uuid、simple-uuid、random-32、random-64、random-128、tik
# 是否输出操作日志
is-log: true
然后就可以开始使用 Sa-Token
提供的简易 API
了。
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);
// 当前会话注销登录
StpUtil.logout();
// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
一旦登录后就可以查询会话和查询凭证。
// 获取当前会话账号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 值
StpUtil.getTokenValue();
// 获取当前 `StpLogic` 的 token 名称
StpUtil.getTokenName();
// 获取指定 token 对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);
// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();
// 获取当前会话的 token 信息参数
StpUtil.getTokenInfo();
重要
补充:使用注解可以用来确认该接口是否需要登录才能使用。您需要配置引入一个注解鉴权的配置。
package com.work.workusercentre.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 注册 Sa-Token 拦截器, 打开注解式鉴权功能
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
然后就可以使用注解添加到控制层方法中来快速检查某个接口是否需要登录才可以使用。
@SaIgnore
代表该接口无需任何校验就可以通过@SaCheckLogin
代表给接口还需要登录才可以通过
3.1.2.权限认证
权限认证包含 权限码值认证
和 角色标识认证
。由于 Sa-token
无法决定用户的角色标识和权限码值,因此采取了接口设计,用户只需要实现两个接口就可以实现权限认证(甚至支持通配符的使用)。
/**
* 自定义权限加载接口实现类
*/
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码值集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 实际上应该把 Id 解析为您在使用 StpUtil.login(Object id) 时注入的 id 的类型, 然后再通过数据库查询该 id 用户对应的权限码值集合后这这里以列表的形式做返回
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
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;
}
}
然后再通过一些 API
做权限校验即可。
// 获取:当前账号所拥有的权限码值集合
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");
// 获取:当前账号所拥有的角色标识集合
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");
重要
补充:在引入了之前注解的配置类,并且还实现了上面的两个接口后,就可以开始使用注解来做权限校验。
@SaCheckRole("admin")
@SaCheckPermission("user:add")
@SaCheckDisable("comment")
3.1.3.用户下线
用户下线分为三种情况:
- 强制注销:和用户自己使用注销接口登出是一样的,会导致
token
被擦除 - 踢人下线:不会导致
token
被擦除而是被打上特定标记,但该用户再次访问会发现被下线 - 顶人下线:这个是
Sa-token
框架自己内部使用的,用来实现单点登录,一般您无需使用
// 强制注销
StpUtil.logout(10001); // 强制指定账号注销下线
StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线
StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线
// 踢人下线
StpUtil.kickout(10001); // 将指定账号踢下线
StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线
// 顶人下线
StpUtil.replaced(10001); // 将指定账号顶下线
StpUtil.replaced(10001, "PC"); // 将指定账号指定端顶下线
StpUtil.replacedByTokenValue("token"); // 将指定 Token 顶下线
3.2.进阶使用
3.2.1.路由鉴权
可以更具路由进行鉴权,可以 查阅官方文档,不过我的项目还不够大型,而且这个挺好配置的,您看着配置就可以,实现一个类的问题而已。
3.2.2.中间缓存
Sa-Token
默认将数据保持在内存中,但是这种模式是有缺陷的,重启就会导致数据丢失,并且无法在分布式环境中共享数据,因此我们可以利用 Redis
来作为中间件缓存,这样哪怕我们的服务重启只要 Redis
还运行着就可以进行数据恢复(哪怕 Redis
自己挂了还有集群和持久化机制来做恢复)。
我使用的是 Spring Boot3
,您需要仔细按照文档进行配置,我的核心依赖和核心配置如下。
<!-- Redis -->
<dependency>
<!-- 提供 Redis 连接池 -->
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Sa-token -->
<dependency>
<!-- 添加关于登录认证、权限认证等模块的支持 -->
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.42.0</version>
</dependency>
<dependency>
<!-- Sa-Token 集成 redis, 并使用 jackson 序列化 -->
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.42.0</version>
</dependency>
<dependency>
<!-- fastjson2 处理 json 数据 -->
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.48</version>
</dependency>
# 配置框架
spring:
## 配置缓存
data:
redis:
database: 2 # Redis 数据库索引(默认为 0, 通常为了方便开发会认为 0 号为生产环境、1 号为测试环境、2 号为开发环境)
host: 127.0.0.1 # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
# password: Qwe54188_ # Redis 服务器连接密码(默认为空)
timeout: 10s # 连接超时时间
lettuce:
pool: # 链接池配置
max-active: 200 # 连接池最大连接数
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
# sa-token 配置
sa-token: # 默开启 Redis 配置将自动支持 Sa-token 使用 Redis 存储认证相关键值对, 想要关掉需要去除相关依赖
## token 名称
token-name: work-user-centre # 同时也是 cookie 名称
## token 有效期
timeout: 2592000 # 单位为秒, 默认 30 天, -1 代表永久有效
## token 最低活跃频率
active-timeout: -1 # 单位:为秒, 如果 token 超过此时间没有访问系统就会被冻结, 默认 -1 代表不限制, 永不冻结
## token 共享
is-share: false # 在多人登录同一账号时, 是否共用一个 token(为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
## 是否允许同一账号多地同时登录
is-concurrent: true # 为 true 时允许一起登录, 为 false 时新登录挤掉旧登录
## token 风格
token-style: uuid # 默认可取值: uuid、simple-uuid、random-32、random-64、random-128、tik
## 是否输出操作日志
is-log: true
然后就可以自动开始集成本地的 Redis
进行缓存和持久了,很简单。
3.2.3.会话级别
Session
是会话中专业的数据缓存组件,通过 Session
我们可以很方便的缓存一些高频读写数据,提高程序性能,在 Sa-token
中会话有三种级别:
Account-Session
: 指的是框架为每个账号id
分配的Session
(在使用登录接口的时候就用到了,无论用户在哪个设备上登录,StpUtil
都会通过相同的账号ID
绑定同一会话)Token-Session
: 指的是框架为每个token
分配的Session
(每个Token
会话有独立的状态数据,互不干扰。如果用户在多个设备登录,每个设备的会话状态是独立的)Custom-Session
: 指的是以一个特定的值
作为SessionId
,来分配的Session
(custom-session-id
是开发者指定的标识符,这样就不再依赖于账号ID
或Token
,而是根据自定义的标识来管理会话,例如以商品id
作为key
为每个商品分配一个Session
,以便于缓存和商品相关的数据,有些时候有些用户没有登录也可以利用这种机制来进行临时缓存,目的是为了解决那些“脱离登录态、又需要有生命周期和结构管理”的缓存需求,有点像是添加了Redis
这种组件的面向对象的视图)
这个不难理解,实际上就是不同方式调用不同的 API
即可。
3.2.4.同端互斥
在同种类型的客户端中,我们需要做到同端互斥,避免某些用户多开客户端在某些场景下(例如抢票)使用脚本进行刷量,这会无限制的加大我们系统的压力。因此我们希望用户至少在同一种客户端上无法多开登录。并且如果用户一直重复登录,我们的 Redis
中会出现大量的 token
记录,这也是一个安全隐患。
不过首先我们需要把配置文件中的 is-concurrent: true
修改为 is-concurrent: false
,然后我们需要有一个工具类用来从请求报文中判断设备的类型,常见的主流设备类型有三种:桌面端、网页端、移动端
,但是我们希望分得更加仔细一些,因此这里分为:pc、miniProgram、pad、mobile
。
这个工具类 Sa=token
貌似没有提供,因此这里给出一个工具类供您使用,注意请求报文的类型,在 Spring Boot3
中发生了一些改动。
package com.work.workusercentre.utils;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Header;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.work.workusercentre.enums.CodeBindMessage;
import com.work.workusercentre.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import jakarta.servlet.http.HttpServletRequest;
/**
* 设备工具类
*/
@Slf4j
public class DeviceUtils {
/**
* 根据请求获取设备信息
* @param request
* @return
*/
public static String getRequestDevice(HttpServletRequest request) {
String userAgentStr = request.getHeader(Header.USER_AGENT.toString());
// 使用 Hutool 解析 UserAgent
UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
if (userAgent == null) {
throw new BusinessException(CodeBindMessage.PARAMS_ERROR, "禁止隐藏设备类型");
}
// 判断设备类型
String device = "pc"; // 是否为 PC
if (isMiniProgram(userAgentStr)) {
device = "miniProgram"; // 是否为小程序
} else if (isPad(userAgentStr)) {
device = "pad"; // 是否为 Pad
} else if (userAgent.isMobile()) {
device = "mobile"; // 是否为手机
}
log.debug("检测一次设备类型为 {}", device);
return device;
}
/**
* 判断是否是小程序
* 一般通过 User-Agent 字符串中的 "MicroMessenger" 来判断是否是微信小程序
**/
private static boolean isMiniProgram(String userAgentStr) {
// 判断 User-Agent 是否包含 "MicroMessenger" 表示是微信环境
return StrUtil.containsIgnoreCase(userAgentStr, "MicroMessenger")
&& StrUtil.containsIgnoreCase(userAgentStr, "MiniProgram");
}
/**
* 判断是否为平板设备
* 支持 iOS(如 iPad)和 Android 平板的检测
**/
private static boolean isPad(String userAgentStr) {
// 检查 iPad 的 User-Agent 标志
boolean isIpad = StrUtil.containsIgnoreCase(userAgentStr, "iPad");
// 检查 Android 平板(包含 "Android" 且不包含 "Mobile")
boolean isAndroidTablet = StrUtil.containsIgnoreCase(userAgentStr, "Android")
&& !StrUtil.containsIgnoreCase(userAgentStr, "Mobile");
// 如果是 iPad 或 Android 平板, 则返回 true
return isIpad || isAndroidTablet;
}
}
然后无论是登入还是登出,都可以设置同端互斥,对于登入来说就会顶调用相同的种类的客户端登录状态,对于注销来说就会把所有的在线端全部顶下线。
3.2.5.脱离曲奇
有些环境是不支持在 HTTP
报文中使用 Cookie
机制的,但是问题是这些端必须支持 HTTP
报文的传输,比如微信小程序就是典型的场景,但是我们对现有的接口还不想使用微信官方的登录作法,还是希望使用 Cookie
机制怎么办?其实针对 Cookie
的特点就可以应对:
Cookie
可由后端控制写入:不能后端控制写入了,就前端自己写入(难点在后端如何将Token
传递到前端)Cookie
每次请求自动提交:每次请求不能自动提交了,那就手动提交(难点在前端如何将Token
传递到后端,同时后端将其读取出来)
因此我一般考虑这种特殊环境下单独进行条件判断,以支持像微信小程序这样的应用进行登录。我们前面不是写了关于用户端种类的判断么,登录接口中就可以根据这个判断对无 Cookie
环境进行另外的登录处理。可以先看看示例代码,然后再到我的项目中进行查阅。
// 登录接口
@RequestMapping("doLogin")
public SaResult doLogin() {
// 第1步,先登录上
StpUtil.login(10001);
// 第2步,获取 Token 相关参数
SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); // 此方法返回一个对象,其有两个关键属性:tokenName和tokenValue
// 第3步,返回给前端
return SaResult.data(tokenInfo);
}
这里再以 uniapp
为例(毕竟这中框架可以运行在多端上,包括微信小程序)。
// 1、首先在登录时,将 tokenValue 存储在本地,例如:
// 在这之前已经调用过登录接口...
uni.setStorageSync('tokenValue', tokenValue);
// 2、在发起ajax请求的地方,获取这个值,并塞到header里
uni.request({
url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。
header: {
"content-type": "application/x-www-form-urlencoded",
"satoken": uni.getStorageSync('tokenValue') // 关键代码, 注意参数名字是 satoken
},
success: (res) => {
console.log(res.data);
}
});
另外,也可以再灵活一些,上面直接使用 "satoken"
这个参数其实写死了。
// 1、首先在登录时,将tokenName和tokenValue一起存储在本地,例如:
// 在这之前已经调用过登录接口...
uni.setStorageSync('tokenName', tokenName);
uni.setStorageSync('tokenValue', tokenValue);
// 2、在发起ajax的地方,获取这两个值, 并组织到head里
var tokenName = uni.getStorageSync('tokenName'); // 从本地缓存读取tokenName值
var tokenValue = uni.getStorageSync('tokenValue'); // 从本地缓存读取tokenValue值
var header = {
"content-type": "application/x-www-form-urlencoded"
};
if (tokenName != undefined && tokenName != '') {
header[tokenName] = tokenValue;
}
// 3、后续在发起请求时将 header 对象塞到请求头部
uni.request({
url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。
header: header,
success: (res) => {
console.log(res.data);
}
});
再稍微封装一下每次请求需要写入的 token
信息函数后,再来调用就完美解决这个问题了。当然需要注意这种方式必须使用 https
协议,否则 token
明文传输会泄露用户信息,这可不是什么好事。
3.2.6.用户封禁
踢人下线、强制注销、顶人下线
功能,可以用于清退账号,不过对于一些严重违规的账号我们可以实施封禁,封禁的原理很简单,就是在数据库中有一个用标识身份的字段(比如 user、admin、ban
),因此我们希望检测到 ban
时就不允许某个用户进行持续的非法访问。
不过在 Sa-tokne
中直接使用接口就可以进行封禁和解封,不需要依赖数据库,不过我们依旧保留这个字段,这样数据库管理员就不需要调用接口就可以直接在数据库中查看某个用户是否被封禁,对于维护来说比较简单。
// 封禁指定账号
StpUtil.disable(10001, 86400);
// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁)
StpUtil.isDisable(10001);
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001);
// 获取指定账号剩余封禁时间,单位:秒,如果该账号未被封禁,则返回-2
StpUtil.getDisableTime(10001);
// 解除封禁
StpUtil.untieDisable(10001);
警告
警告:值得注意的是,旧版本的 Sa-token
在 StpUtil.login()
时会自动校验账号是否被封禁,v1.31.0
之后将 校验封禁
和 登录
两个动作分离成两个方法,登录接口不再自动校验,请注意其中的逻辑更改!
3.1.7.单点登录
解决了一些无 Cookie
环境的问题后,我们对端的设备就毫无畏惧了,那么就可以视所有客户端都是相同的环境,此时我们就可以考虑单点登录的问题了。简而言之**:在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统,也就是 SSO 模块**。尤其是对同一公司的网站,明明共用一个用户数据库,却要求用户都要手动登录,很影响用户体验。对此 Sa-token
有三种应对场景,以及对应的解决方案。对于小型公司来说,第一种方案会比较常用,我这里只考虑第一种情况,其他情况查阅官方文档就可以了。
系统架构 | 采用模式 | 简介 | 文档链接 |
---|---|---|---|
前端同域 + 后端同 Redis | 模式一 | 共享 Cookie 同步会话 | 文档、示例 |
前端不同域 + 后端同 Redis | 模式二 | URL 重定向传播会话 | 文档、示例 |
前端不同域 + 后端不同 Redis | 模式三 | Http 请求获取会话 | 文档、示例 |
首先我们来根据第一个方案来进行拆解,我们先搞清楚为什么我们无法在多个同根域名的项目里无法共享登录状态,并且给出对应的解决方案:
- 登陆后返回给前端的
Token
无法在多个系统下共享 -> 使用共享Cookie
来解决Token
共享问题。所谓共享Cookie
,就是主域名Cookie
在二级域名下的共享,举个例子:写在父域名stp.com
下的Cookie
,在s1.stp.com、s2.stp.com
等子域名都是可以共享访问的 - 登陆后存储在后端的
Session
无法在多个系统间共享 -> 使用Redis
来解决Session
共享问题。而共享Redis
,本来时需要共享一个Redis
集群的,但是其实并不需要我们把所有项目的数据都放在同一个Redis
中,Sa-Token
提供了 权限缓存与业务缓存分离 的解决方案 Alone 独立 Redis 插件,感兴趣可以看一看,这里还是先使用单个的Redis
主节点来做演示。
接下来就是开始实践的阶段,这一部分我推荐直接按照官方文档来搭建,如果您希望一点点改造,则可以从 Sa-token
中拉取官方示例(文档有提到 具体地址)我们需要按照以下的步骤:
- 搭建统一认证中心服务,请 参考官方示例代码 sa-token-demo-sso-server
- 搭建多个同域根客户端,请 参考官方示例代码 sa-token-demo-sso1-client
- 配置本地域名解析文件:在
windows
下的C:\windows\system32\drivers\etc\hosts
或linux
下的/etc/hosts
文件中添加以下域名解析规则
```bash
127.0.0.1 sso.stp.com # 注意官方的示例代码打印的地址是不对的应该访问这个(这个注释去除掉)
127.0.0.1 s1.stp.com
127.0.0.1 s2.stp.com
127.0.0.1 s3.stp.com
```
其中 `sso.stp.com` 为统一认证中心地址,当用户在其它 `Client` 端发起登录请求时,均将其重定向至认证中心,待到登录成功之后再原路返回到 `Client` 端
我们先来看看成果,再来分析做了什么,首先官方给的上述两个示例仓库是使用一个前端项目模拟三个不同的子域名,这些子域名的前端都是普通的 h5
页面,在点击登录按钮后,其他子域名的页面都会自动登录。




然后刷新其他页面就会发现自动进行了登录。


更多的地方以后研究,待补充...
3.2.8.三方登录
OAuth2.0
与 SSO
相比,增加了对应用授权范围的控制,减弱了应用之间数据同步的能力。待补充...
3.2.9.用户记录
有些时候我们希望登录后,用户可以选中“记住登录”,以支持保持登录状态,但是其实 Sa-token
默认支持的就是记住登录,因此我们的重心反而是如何让用户在不勾选的时候记不住登录,待补充...
3.2.10.密码加密
Sa-token
还提供了强大的密码加密工具包,可以简化对用户密码加密的过程,待补充...