10 KiB
本小节中,我们将为网关服务添加一个过滤器,实现将当前用户 ID 透传给下游服务。
为什么需要透传用户 ID ?以什么方式传?
那么,问题来了,为什么网关在路由转发时,需要透传用户 ID 给下游服务呢? 就以认证服务中的用户退出登录接口为例,当该接口被请求时,如果网关不告诉当前请求的用户 ID, 认证服务不知道实际是要退出哪个用户的。
第二个问题来了,要以什么样的方式传给子服务呢? 由于服务之间的通信方式是 HTTP , 网关服务在接受到请求时,可以依据 Token 令牌解析到当前用户 ID, 转发路由时,再将用户 ID 添加到 Header 请求头中,这样,下游服务接受到请求时,直接从请求头中获取用户 ID 即可。
添加过滤器
接下来,我们就来实现这个功能。编辑网关服务,创建一个 /filter 包,用于统一放置过滤器相关代码,并新建 AddUserId2HeaderFilter 过滤器,代码如下:
package com.quanxiaoha.xiaohashu.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author: 犬小哈
* @date: 2024/4/9 15:52
* @version: v1.0.0
* @description: 转发请求时,将用户 ID 添加到 Header 请求头中,透传给下游服务
**/
@Component
@Slf4j
public class AddUserId2HeaderFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("==================> TokenConvertFilter");
// 将请求传递给过滤器链中的下一个过滤器进行处理。没有对请求进行任何修改。
return chain.filter(exchange);
}
}
解释一波:
GlobalFilter: 这是一个全局过滤器接口,会对所有通过网关的请求生效。filter()入参解释:
ServerWebExchange exchange:表示当前的 HTTP 请求和响应的上下文,包括请求头、请求体、响应头、响应体等信息。可以通过它来获取和修改请求和响应。GatewayFilterChain chain:代表网关过滤器链,通过调用chain.filter(exchange)方法可以将请求传递给下一个过滤器进行处理。chain.filter(exchange): 将请求传递给过滤器链中的下一个过滤器进行处理。当前没有对请求进行任何修改。
过滤器执行顺序
细心的小伙伴,应该注意到了,在之前的 SaToken 配置类中,也是配置了一个过滤器 SaReactorFilter , 如下图所示:
当同时定义了两个过滤器,执行顺序是怎么样的?查看 SaReactorFilter源码,如下图所示,可以看到此过滤器添加了一个 @Order(-100) 注解:
@Order注解的作用:用于定义 Spring 组件的加载顺序。在过滤器的上下文中,它用来指定过滤器的执行顺序。Spring 容器会按照@Order注解中定义的顺序执行过滤器。@Order注解的数值含义:注解后面的数值表示优先级,数值越小,优先级越高。即:
- 数值越小,过滤器的执行顺序越靠前(优先执行)。
- 数值越大,过滤器的执行顺序越靠后(后执行)。
由于添加了 @Order(-100) 注解,SaReactorFilter 过滤器的优先级非常之高,相对于未指定数值的 AddUserId2HeaderFilter 过滤器。为了验证一下,咱们在 SaTokenConfigure 过滤器中添加一行日志,代码如下:
package com.quanxiaoha.xiaohashu.gateway.auth;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**") /* 拦截全部path */
// 鉴权方法:每次访问进入
.setAuth(obj -> {
log.info("==================> SaReactorFilter, Path: {}", SaHolder.getRequest().getRequestPath());
// 省略...
})
// 省略...
;
}
}
日志添加完毕后,重启网关服务,通过 Apipost 工具调试一波登出接口:
观察网关服务的控制台日志,可以看到确实是 SaReactorFilter 先被执行,然后才执行到 AddUserId2HeaderFilter 过滤器:
这样就可以保证,权限校验在前面,真正执行到 AddUserId2HeaderFilter 过滤器中时,要么是接口校验通过了,要么是该接口无需校验两种情况。
自定义过滤器逻辑
接着,编辑 AddUserId2HeaderFilter 过滤器,开始编写添加用户 ID 到请求头的逻辑,代码如下:
package com.quanxiaoha.xiaohashu.gateway.filter;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author: 犬小哈
* @date: 2024/4/9 15:52
* @version: v1.0.0
* @description: 转发请求时,将用户 ID 添加到 Header 请求头中,透传给下游服务
**/
@Component
@Slf4j
public class AddUserId2HeaderFilter implements GlobalFilter {
/**
* 请求头中,用户 ID 的键
*/
private static final String HEADER_USER_ID = "userId";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("==================> TokenConvertFilter");
// 用户 ID
Long userId = null;
try {
// 获取当前登录用户的 ID
userId = StpUtil.getLoginIdAsLong();
} catch (Exception e) {
// 若没有登录,则直接放行
return chain.filter(exchange);
}
log.info("## 当前登录的用户 ID: {}", userId);
Long finalUserId = userId;
ServerWebExchange newExchange = exchange.mutate()
.request(builder -> builder.header(HEADER_USER_ID, String.valueOf(finalUserId))) // 将用户 ID 设置到请求头中
.build();
return chain.filter(newExchange);
}
}
解释一波代码:
StpUtil.getLoginIdAsLong();: 通过 SaToken 工具类获取当前用户 ID。如果请求中携带了 Token 令牌,则会获取成功;如果未携带,能执行到这里,说明请求的接口无需权限校验,这个时候获取用户 ID 会报抛异常,catch()到异常后,不做任何修改,将请求传递给过滤器链中的下一个过滤器,发行请求即可;- 如果成功拿到了用户 ID, 则开始添加用户 ID 到请求头操作:
ServerWebExchange newExchange = exchange.mutate():通过mutate()方法创建一个新的ServerWebExchange对象,用于修改当前请求。.request(builder -> builder.header(HEADER_USER_ID, String.valueOf(finalUserId))):修改请求头,添加用户 ID;.build();:构建修改后的ServerWebExchange对象。return chain.filter(newExchange);:将修改后的newExchange对象传递给过滤器链中的下一个过滤器进行处理。
下游服务获取用户 ID
接着,编辑认证服务中的 /logout 登出接口,获取一下请求头中的用户 ID , 并打印日志,代码如下:
@PostMapping("/logout")
@ApiOperationLog(description = "账号登出")
public Response<?> logout(@RequestHeader("userId") String userId) {
// todo 账号登录逻辑待实现
log.info("==> 网关透传过来的用户 ID: {}", userId);
return Response.success();
}
测试一波
完成以上编码工作后,分别重启网关服务与认证服务。通过 Apipost 工具,先测试一下登出接口,目前对登出接口配置了校验如下权限:
SaRouter.match("/auth/user/logout", r -> StpUtil.checkPermission("app:note:publish"));
看看认证服务接受到转发的请求时,是否能够拿到请求头中透传过来的用户 ID, 如下图所示,通过日志信息,OK, 成功拿到了用户 ID, 功能正常:
再来测试一下接口不携带 Token 令牌的情况,如获取验证码接口,看看网关转发功能是否正常:
从上图可以看出,请求没有携带 Token 令牌,并且 SaReactorFilter 权限校验通过的请求,AddUserId2HeaderFilter 也是工作正常良好,并将路由请求转发到了认证服务上。