将Sa-Token集成到Spring Cloud Gateway实现网关统一鉴权
项目地址
一、整体思路与架构
对Sa-Token做了一个独立封装模块 aimin-satoken,然后在各个微服务(包括 aimin-gateway网关)中引入这个模块,实现统一的登录逻辑和配置。
整体鉴权流程如下:
- 用户/管理员在对应服务(auth/admin)登录,由Sa-Token生成Token(
aimin-auth-token/ aimin-admin-token)。
- 所有请求先经过Spring Cloud Gateway。
- 网关中注册一个全局过滤器
SaReactorFilter:
- 拦截所有请求
- 根据请求路径(
/aimin-auth/**、/aimin-admin/**等)选择不同的鉴权策略
- 调用
StpUserUtil或StpAdminUtil进行登录校验
- 校验通过后,才放行到真实微服务。
二、从引入依赖开始
在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-gateway的bootstrap.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(); }) .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"); StpAdminUtil.stpLogic.checkLogin(); } }
|
用户端策略:UserPathStrategy
1 2 3 4 5 6 7 8
| public class UserPathStrategy implements LoginCheckStrategy {
@Override public void checkAuth() { StpUserUtil.stpLogic.checkLogin(); } }
|
PublicPathStrategy 很简单,大概率就是一个空实现(直接放行):
1 2 3 4 5 6
| public class PublicPathStrategy implements LoginCheckStrategy { @Override public void checkAuth() { } }
|
四、一次完整请求的鉴权流程
以请求后台接口/aimin-admin/user/list为例:
- 浏览器/前端带着
aimin-admin-token=xxx请求网关。
- Gateway收到请求 →
SaReactorFilter拦截。
setAuth()中拿到请求路径/aimin-admin/user/list。
- 调用
StrategyFactory.getStrategy("/aimin-admin/user/list"):
- 匹配上
/aimin-admin/** → 返回 AdminPathStrategy。
- 执行
AdminPathStrategy.checkAuth():
- 内部执行
StpAdminUtil.stpLogic.checkLogin()。
- Sa-Token会从请求头 / Cookie / param 中解析
aimin-admin-token,判断是否已登录。
- 已登录 → 放行请求到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 {
public static StpLogic stpLogic = new StpLogic("ADMIN");
public static Integer getLoginId() { Object loginId = stpLogic.getLoginId(); if (loginId == null) { throw BusinessException.of("admin未登录"); } return Integer.parseInt(loginId.toString()); }
public static SaTokenInfo loginByPc(Integer id) { SaLoginModel config = new SaLoginModel(); config.setDevice(Device.PC); config.setIsWriteHeader(false); stpLogic.login(id, config); return getTokenInfo(); }
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 {
public static StpLogic stpLogic = new StpLogic("USER");
public static String getLoginId() { Object loginId = stpLogic.getLoginId(); if (loginId == null) { throw BusinessException.of("用户未登录"); } return loginId.toString(); }
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); 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