网管关统一鉴权 | sa-token.cc
对于网关服务,大体来讲分为两种:
sa-token-spring-boot-starter
。sa-token-reactor-spring-boot-starter
,并且注册全局过滤器!注意:切不可直接在一个项目里同时引入这两个依赖,否则会造成项目无法启动
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
注意:如果你使用的是
SpringBoot 3.x
,只需要将sa-token-spring-boot-starter
修改为sa-token-spring-boot3-starter
即可。
<!-- Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
注意:如果你使用的是
SpringBoot 3.x
,只需要将sa-token-reactor-spring-boot-starter
修改为sa-token-reactor-spring-boot3-starter
即可。
无论使用哪种序列化方式,你都必须为项目提供一个 Redis 实例化方案,因为我们需要和各个服务通过Redis来同步数据。
优点:兼容性好,缺点:Session 序列化后基本不可读,对开发者来讲等同于乱码。
<!-- Sa-Token 整合 Redis (使用 jdk 默认序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis</artifactId>
<version>1.35.0.RC</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
优点:Session 序列化后可读性强,可灵活手动修改,缺点:兼容性稍差。
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.35.0.RC</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33groupId>
<artifactId>sa-token-jwtartifactId>
<version>1.35.0.RCversion>
dependency>
- 注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本,保险起见:你的项目中要么不引入 hutool,要么引入版本 >= 5.7.14 的 hutool 版本。
- hutool 5.8.13 和 5.8.14 版本下会出现类型转换问题,关联issue。
在 application.yml
配置文件中配置 jwt 生成秘钥:
sa-token:
# jwt秘钥
jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
根据不同的整合规则,插件提供了三种不同的模式,你需要选择其中一种注入到你的项目中
@Configuration
public class SaTokenConfigure {
// Sa-Token 整合 jwt (Simple 简单模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
// Sa-Token 整合 jwt (Mixin 混入模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForMixin();
}
// Sa-Token 整合 jwt (Stateless 无状态模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForStateless();
}
}
关于数据的获取,建议以下方案三选一:
放到登录服务中:
/**
* 自定义权限验证接口扩展
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Autowired
private MenuService menuService;
@Autowired
private RoleService roleService;
@Autowired
private ThreadPoolConfig threadPoolConfig;
/**
* 返回一个账号所拥有的权限码集合
*
* @param loginId
* @param loginType
* @return
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
List<String> res = (List<String>) StpUtil.getTokenSession().get("PERMISSION-LIST");
if (res == null) {
CompletableFuture<List<String>> permissionFuture = CompletableFuture.supplyAsync(() -> {
List<MenuVO> menuVOList = menuService.getPermissionList(Convert.toLong(loginId),null);
return menuVOList.stream().map(MenuVO::getPermission).collect(Collectors.toList());
}, threadPoolConfig.USER_ROLE_PERM_THREAD_POOL);
try {
return permissionFuture.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
return res;
}
/**
* 返回一个账号所拥有的角色标识集合
*
* @param loginId
* @param loginType
* @return
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
List<String> res = (List<String>) StpUtil.getTokenSession().get("ROLE-LIST");
if (res == null) {
CompletableFuture<List<String>> roleFuture = CompletableFuture.supplyAsync(() -> {
// 返回此 loginId 拥有的权限列表
List<RoleVO> roles = roleService.getRoleByUserId(Convert.toLong(loginId));
return roles.stream().map(RoleVO::getRoleKey).collect(Collectors.toList());
}, threadPoolConfig.USER_ROLE_PERM_THREAD_POOL);
try {
return roleFuture.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
return res;
}
}
其他子服务:
/**
* 自定义权限验证接口扩展
*/
@Component
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*
* @param loginId
* @param loginType
* @return
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return (List<String>) StpUtil.getTokenSession().get("PERMISSION-LIST");
}
/**
* 返回一个账号所拥有的角色标识集合
*
* @param loginId
* @param loginType
* @return
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return (List<String>) StpUtil.getTokenSession().get("ROLE-LIST");
}
}
然后在网关处注册全局过滤器进行鉴权操作
@Configuration
public class SaTokenConfigure{
/**
* Sa-Token 整合 jwt (Simple 简单模式)
*
* @return
*/
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
/**
* 注册 [Sa-Token全局过滤器]
*
* @return
*/
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址 - 拦截全部path
.addInclude("/**")
// 开放地址
.addExclude("/favicon.png")
// 鉴权方法:每次访问进入
// 全局认证函数
.setAuth(obj -> {
SaRouter
// 拦截的所有接口
.match("/**")
// 忽略所有登陆相关接口
.notMatch("/account/**")
// 忽略所有个人信息相关接口
.notMatch("/manage/profile/**")
// 忽略获得文件下载链接相关接口
.notMatch("/file/fileTransfer/getFileUrl/**")
// 忽略获取站点信息列表相关接口
.notMatch("/manage/config/get")
// 忽略获取所有的公告相关接口
.notMatch("/manage/notice/list")
// 忽略所有接口文档相关接口
.notMatch("/doc.html",
"/doc.html*",
"/doc.html/*",
"/webjars/**",
"/img.icons/**",
"/swagger-resources/**",
"/**/v2/api-docs")
// 要执行的校验动作,可以写完整的 lambda 表达式
.check(r -> StpUtil.checkLogin());
})
// 异常处理函数
.setError(e -> {
return ResultResponse.fail().message(e.getMessage());
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
// ---------- 设置跨域响应头 ----------
SaHolder.getResponse()
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理--------"))
.back();
});
}
}
为网关添加全局过滤器:此过滤器会为Request请求头追加 Same-Token
参数,这个参数会被转发到子服务。
/**
* 全局过滤器,为请求添加 Same-Token
*/
@Component
public class ForwardAuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest newRequest = exchange
.getRequest()
.mutate()
// 为请求追加 Same-Token 参数
.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
.build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
}
}
在子服务添加过滤器校验参数。
/**
* Sa-Token 权限认证 配置类
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 全局过滤器
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/favicon.ico")
.setAuth(obj -> {
// 校验 Same-Token 身份凭证 —— 以下两句代码可简化为:SaSameUtil.checkCurrentRequestToken();
String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN);
SaSameUtil.checkToken(token);
})
.setError(e -> {
return SaResult.error(e.getMessage());
})
;
}
}
启动网关与子服务,访问测试:
如果通过网关转发,可以正常访问,直接访问子服务会提示:
无效Same-Token:xxx
有时候我们需要在一个服务调用另一个服务的接口,这也是需要添加Same-Token
作为身份凭证的
在服务里添加 Same-Token 流程与网关类似,我们以RPC框架 Feign
为例:
/**
* feign拦截器, 在feign请求发出之前,加入一些操作
*/
@Component
public class FeignInterceptor implements RequestInterceptor {
// 为 Feign 的 RCP调用 添加请求头Same-Token
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
// 如果希望被调用方有会话状态,此处就还需要将 satoken 添加到请求头中
// requestTemplate.header(StpUtil.getTokenName(), StpUtil.getTokenValue());
}
}
/**
* 在调用接口里使用 Interceptor
*/
@FeignClient(name = "netdisk-account", configuration = FeignInterceptor.class)
public interface AccountFeignService {
/**
* 返回一个账号所拥有的权限码集合
*
* @return
*/
@RequestMapping("/menu/getPermissionList/")
ResultResponse<List<MenuVO>> getPermissionList();
/**
* 根据用户ID获取详细信息
*
* @param userId
* @return
*/
@RequestMapping("/role/getRoleByUserId/{userId}")
ResultResponse<List<RoleVO>> getRoleByUserId(@PathVariable Long userId);
}