将Sa-Token集成到Spring Cloud Gateway实现网关统一鉴权

将Sa-Token集成到Spring Cloud Gateway实现网关统一鉴权

项目地址

一、整体思路与架构

对Sa-Token做了一个独立封装模块 aimin-satoken,然后在各个微服务(包括 aimin-gateway网关)中引入这个模块,实现统一的登录逻辑和配置。

整体鉴权流程如下:

  1. 用户/管理员在对应服务(auth/admin)登录,由Sa-Token生成Token(aimin-auth-token/ aimin-admin-token)。
  2. 所有请求先经过Spring Cloud Gateway
  3. 网关中注册一个全局过滤器SaReactorFilter
    • 拦截所有请求
    • 根据请求路径(/aimin-auth/**/aimin-admin/**等)选择不同的鉴权策略
    • 调用StpUserUtilStpAdminUtil进行登录校验
  4. 校验通过后,才放行到真实微服务。

二、从引入依赖开始

在aimin-satoken中引入以下satoken相关的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>

<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>

<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
</dependency>

在其他的微服务例如:aimin-admin/aimin-gateway,引入aimin-satoken的依赖即可

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.oimc</groupId>
<artifactId>aimin-satoken</artifactId>
<exclusions>
<exclusion>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
</exclusion>
</exclusions>
</dependency>

三、在Gateway中集成Sa-Token统一鉴权

3.1 配置白名单路径(security.ignore)

aimin-gatewaybootstrap.yml中,配置了白名单:用户访问这些地址是不受管控的

1
2
3
4
5
6
7
security:
ignore:
whites:
- /aimin-auth/public/wx/token
- /aimin-admin/public/auth/token
- /aimin-admin/public/auth/captchaVerify
- /public/**

配套的配置类:IgnoreWhiteProperties

1
2
3
4
5
6
7
8
9
10
@Data
@NoArgsConstructor
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties {

private List<String> whites = new ArrayList<>();

}

这样可以把白名单放在配置中心(比如Nacos)里,通过 @RefreshScope 热更新。

3.2 网关注册Sa-Token全局过滤器:SaReactorFilter

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
@Configuration
public class SaTokenConfigure {

@Bean
public SaReactorFilter getSaReactorFilter() {

return new SaReactorFilter()
// 排除无需权限的接口(这里额外写了一个)
.addExclude("/aimin-admin/public/**")
// 拦截所有路由
.addInclude("/**")
// 认证函数:每次请求都会执行
.setAuth(obj -> {
String path = SaHolder.getRequest().getRequestPath();
StrategyFactory.getStrategy(path).checkAuth();
})
// 异常处理:统一返回 JSON 错误信息
.setError(e -> {
logger.log(Level.WARNING,"sa全局异常",e);
return SaResult.error(e.getMessage());
})
// 前置处理:统一设置跨域
.setBeforeAuth(obj -> {
SaHolder.getResponse()
.setHeader("Access-Control-Allow-Origin", "*")
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, HEAD")
.setHeader("Access-Control-Max-Age", "3600")
.setHeader("Access-Control-Allow-Headers", "*");
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> {})
.back();
});
}
}

关键点:

  • addInclude("/**")拦截所有请求
  • setAuth()中只做了一件事:交给StrategyFactory按路径选择鉴权策略

3.3 路径策略:根据路径决定使用哪套登录体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StrategyFactory {

private static final AntPathMatcher pathMatcher = new AntPathMatcher();

// 路由 -> 策略 的映射(有顺序)
private static final Map<String, LoginCheckStrategy> strategyMap = new LinkedHashMap<>();

static {
strategyMap.put("/aimin-auth/public/**", new PublicPathStrategy());
strategyMap.put("/aimin-admin/public/**", new PublicPathStrategy());
strategyMap.put("/aimin-auth/**", new UserPathStrategy());
strategyMap.put("/aimin-admin/**", new AdminPathStrategy());
}

public static LoginCheckStrategy getStrategy(String requestPath) {
for (Map.Entry<String, LoginCheckStrategy> entry : strategyMap.entrySet()) {
if (pathMatcher.match(entry.getKey(), requestPath)) {
return entry.getValue();
}
}
return new PublicPathStrategy(); // 默认策略:放行
}
}

说明:

  • /aimin-auth/public/**/aimin-admin/public/** → 公共接口,走 PublicPathStrategy,不校验登录。
  • /aimin-auth/**用户端接口,走 UserPathStrategy,需要用户登录。
  • /aimin-admin/**管理端接口,走 AdminPathStrategy,需要管理员登录。

3.4 具体策略实现:AdminPathStrategy & UserPathStrategy

管理端策略:AdminPathStrategy

1
2
3
4
5
6
7
8
9
10
@Slf4j
public class AdminPathStrategy implements LoginCheckStrategy {

@Override
public void checkAuth() {
log.info("admin path check auth");
// 调用 Sa-Token 的 ADMIN 体系进行登录校验
StpAdminUtil.stpLogic.checkLogin();
}
}

用户端策略:UserPathStrategy

1
2
3
4
5
6
7
8
public class UserPathStrategy implements LoginCheckStrategy {

@Override
public void checkAuth() {
// 调用 Sa-Token 的 USER 体系进行登录校验
StpUserUtil.stpLogic.checkLogin();
}
}

PublicPathStrategy 很简单,大概率就是一个空实现(直接放行):

1
2
3
4
5
6
public class PublicPathStrategy implements LoginCheckStrategy {
@Override
public void checkAuth() {
// do nothing, 直接通过
}
}

四、一次完整请求的鉴权流程

以请求后台接口/aimin-admin/user/list为例:

  1. 浏览器/前端带着 aimin-admin-token=xxx请求网关。
  2. Gateway收到请求 → SaReactorFilter拦截。
  3. setAuth()中拿到请求路径/aimin-admin/user/list
  4. 调用StrategyFactory.getStrategy("/aimin-admin/user/list")
    • 匹配上 /aimin-admin/** → 返回 AdminPathStrategy
  5. 执行AdminPathStrategy.checkAuth()
    • 内部执行StpAdminUtil.stpLogic.checkLogin()
    • Sa-Token会从请求头 / Cookie / param 中解析aimin-admin-token,判断是否已登录。
  6. 已登录 → 放行请求到aimin-admin微服务。
    未登录 → 抛出异常 → 被setError()捕获并返回统一的JSON错误(比如”admin未登录”)。

五、 整体流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌───────────────┐
│ 网关收到请求 │
└───────┬───────┘


SaReactorFilter(全局过滤器)



StrategyFactory 根据路径选择登录策略


├── /aimin-admin/** → AdminPathStrategy → StpAdminUtil.checkLogin()


├── /aimin-auth/** → UserPathStrategy → StpUserUtil.checkLogin()


└── /public/** → PublicPathStrategy(放行)



权限检查通过 → 放行到具体微服务

附录:封装 Sa-Token 公共组件

管理员端工具类:StpAdminUtil

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
36
37
38
public class StpAdminUtil {

// ADMIN 命名空间,代表“管理员登录体系”
public static StpLogic stpLogic = new StpLogic("ADMIN");

// 获取当前登录管理员 ID
public static Integer getLoginId() {
Object loginId = stpLogic.getLoginId();
if (loginId == null) {
throw BusinessException.of("admin未登录");
}
return Integer.parseInt(loginId.toString());
}

// PC 端登录(后台管理)
public static SaTokenInfo loginByPc(Integer id) {
SaLoginModel config = new SaLoginModel();
config.setDevice(Device.PC);
config.setIsWriteHeader(false);
stpLogic.login(id, config);
return getTokenInfo();
}

// 获取 Token 信息
public static SaTokenInfo getTokenInfo() {
return stpLogic.getTokenInfo();
}

// 退出登录
public static void logout() {
stpLogic.logout();
}

// 获取权限列表
public static List<String> permissions() {
return stpLogic.getPermissionList();
}
}

用户端工具类:StpUserUtil

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
36
37
38
39
public class StpUserUtil {

// USER 命名空间,代表“用户登录体系”(小程序 / H5)
public static StpLogic stpLogic = new StpLogic("USER");

// 获取当前登录用户 openId
public static String getLoginId() {
Object loginId = stpLogic.getLoginId();
if (loginId == null) {
throw BusinessException.of("用户未登录");
}
return loginId.toString();
}

// PC 端登录(如果有 Web 用户)
public static void loginByPc(String openId) {
SaLoginModel config = new SaLoginModel();
config.setDevice(Device.PC);
config.setIsWriteHeader(false);
stpLogic.login(openId, config);
}

// 小程序登录
public static SaTokenInfo loginByMiniProgram(String openId) {
SaLoginModel config = new SaLoginModel();
config.setDevice(Device.MINI_PROGRAM);
config.setIsWriteHeader(false);
stpLogic.login(openId, config);
return getTokenInfo();
}

public static SaTokenInfo getTokenInfo() {
return stpLogic.getTokenInfo();
}

public static Boolean isLogin(){
return stpLogic.isLogin();
}
}

两套体系的 Token 配置:StpUtilConfig

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
public class StpUtilConfig {
public void init() {
// 管理端配置
SaTokenConfig adminConfig = new SaTokenConfig();
adminConfig.setTokenName("aimin-admin-token");
adminConfig.setTimeout(2592000); // 30 天
adminConfig.setActiveTimeout(-1L);
adminConfig.setIsConcurrent(false); // 禁止并发登录
adminConfig.setIsShare(true);
adminConfig.setIsLog(true);
adminConfig.setTokenStyle("random-64");
StpAdminUtil.stpLogic.setConfig(adminConfig);

// 用户端配置
SaTokenConfig userConfig = new SaTokenConfig();
userConfig.setTokenName("aimin-auth-token");
userConfig.setTimeout(2592000);
userConfig.setActiveTimeout(-1);
userConfig.setIsConcurrent(false);
userConfig.setIsShare(true);
userConfig.setIsLog(true);
userConfig.setTokenStyle("random-64");
StpUserUtil.stpLogic.setConfig(userConfig);
}
}

自动配置:SaTokenAutoConfiguration

1
2
3
4
5
6
7
8
@Configuration
public class SaTokenAutoConfiguration {

@Bean(initMethod = "init")
public StpUtilConfig setSaTokenConfig() {
return new StpUtilConfig();
}
}

只要在微服务中引入aimin-satoken依赖,Spring Boot启动时就会自动创建Bean,并执行init(),把Sa-Token的配置塞进Admin/User两套登录体系。

END