在微服务中使用 Sa-Token:分布式Session会话、网关统一鉴权
Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
Gitee 开源地址:https://gitee.com/dromara/sa-token
本篇将介绍在微服务中使用 Sa-Token 的两种常见技术:
- 分布式 Session 会话。
- 网关统一鉴权。
一、分布式 Session 会话
微服务架构下的第一个难题便是数据同步,单机版的Session在分布式环境下一般不能正常工作,为此我们需要对框架做一些特定的处理。
首先我们要明白,分布式环境下为什么Session会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点,
这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。
要怎么解决这个问题呢?目前的主流方案有四种:
1. Session同步:只要一个节点的数据发生了改变,就强制同步到其它所有节点
2. Session粘滞:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务
3. 建立会话中心:将Session存储在专业的缓存中间件上,使每个节点都变成了无状态服务,例如:Redis
4. 颁发无状态token:放弃Session机制,将用户数据直接写入到令牌本身上,使会话数据做到令牌自解释,例如:jwt
使用 Sa-Token 可以为项目构建一个会话中心模式,所有的数据都统一放在 Redis 中,每个服务都是无状态服务,保证会话数据的一致性。
动态演示图:

在每个服务中引入以下依赖(或根pom统一引入)
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.34.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
如上述依赖所示,只要所有客户端配置一样的 Redis 链接,他们的会话就可以互通。
如果你的业务无法统一配置一个 Redis,也可以使用 Alone-Redis 插件,为 Sa-Token 单独配置一个Redis 依赖,做到权限缓存与业务缓存分离:
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>1.34.0</version>
</dependency>- 1
- 2
- 3
- 4
- 5
- 6
然后在application.yml中增加配置
# Sa-Token 配置
sa-token:
# Token名称
token-name: satoken
# Token有效期
timeout: 2592000
# Token风格
token-style: uuid
# 配置 Sa-Token 单独使用的 Redis 连接
alone-redis:
# Redis数据库索引(默认为0)
database: 2
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s
spring:
# 配置业务使用的 Redis 连接
redis:
# Redis数据库索引(默认为0)
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
新建Controller测试一下:
@RestController
@RequestMapping("/test/")
public class TestController {
@Autowired
StringRedisTemplate stringRedisTemplate;
// 测试Sa-Token缓存
@RequestMapping("login")
public SaResult login(@RequestParam(defaultValue="10001") String id) {
System.out.println("--------------- 测试Sa-Token缓存");
StpUtil.login(id);
return SaResult.ok();
}
// 测试业务缓存
@RequestMapping("test")
public SaResult test() {
System.out.println("--------------- 测试业务缓存");
stringRedisTemplate.opsForValue().set("hello", "Hello World");
return SaResult.ok();
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
分别访问两个接口,观察Redis中增加的数据:

二、网关统一鉴权
微服务架构下的鉴权一般分为两种:
- 每个服务各自鉴权。
- 网关统一鉴权。
方案一和传统单体鉴权差别不大,不再过多赘述,下面介绍方案二的整合步骤:
1、引入正确的依赖:
在交流群里不少小伙伴提出bug解决到最后发现都是因为依赖引入错误导致的,此处重点强调一下:
在微服务架构中使用Sa-Token时,网关和内部服务要分开引入Sa-Token依赖(不要直接在顶级父pom中引入Sa-Token)
总体来讲,我们需要关注的依赖就是两个:sa-token-spring-boot-starter 和 sa-token-reactor-spring-boot-starter:
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>- 1
- 2
- 3
- 4
- 5
- 6
<!-- Sa-Token 权限认证(Reactor响应式集成) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>- 1
- 2
- 3
- 4
- 5
- 6
至于怎么分辨我们需要引入哪个呢?这个要看你使用的基础框架:
对于内部基础服务来讲,我们一般都是使用SpringBoot默认的web模块:SpringMVC,
因为这个SpringMVC是基于Servlet模型的,在这里我们需要引入的是sa-token-spring-boot-starter
对于网关服务,大体来讲分为两种:
- 一种是基于Servlet模型的,如:Zuul,我们需要引入的是:sa-token-spring-boot-starter,详细戳:在SpringBoot环境集成
- 一种是基于Reactor模型的,如:SpringCloud Gateway,我们需要引入的是:sa-token-reactor-spring-boot-starter。
注:切不可直接在一个项目里同时引入这两个依赖,否则会造成项目无法启动。
另外,我们需要引入Redis集成包,因为我们的网关和子服务主要通过Redis来同步数据
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.34.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
2、实现鉴权接口
/**
* 自定义权限验证接口扩展
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 返回此 loginId 拥有的权限列表
return ...;
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 返回此 loginId 拥有的角色列表
return ...;
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
关于数据的获取,建议以下方案三选一:
1. 在网关处集成ORM框架,直接从数据库查询数据
2. 先从Redis中获取数据,获取不到时走ORM框架查询数据库
3. 先从Redis中获取缓存数据,获取不到时走RPC调用子服务 (专门的权限数据提供服务) 获取
3、注册全局过滤器
然后我们就可以在网关处注册全局过滤器进行鉴权操作了:
/**
* [Sa-Token 权限认证] 配置类
* @author kong
*/
@Configuration
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**") /* 拦截全部path */
// 开放地址
.addExclude("/favicon.ico")
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
// 权限认证 -- 不同模块, 校验不同权限
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"));
// 更多匹配 ... */
})
// 异常处理方法:每次setAuth函数出现异常时进入
.setError(e -> {
return SaResult.error(e.getMessage());
})
;
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
在此处,你可以按照 路由拦截鉴权 模式进行鉴权。

参考资料
- Sa-Token 文档:https://sa-token.com
- Gitee 仓库地址:https://gitee.com/dromara/sa-token
- GitHub 仓库地址:https://github.com/dromara/sa-token
鲁公网安备37011202002956号