weblog/doc/7、Gateway 网关搭建与接口鉴权/7.7 网关过滤器:实现用户 ID 透传到下游服务.md
2025-02-17 11:57:55 +08:00

10 KiB
Raw Blame History

本小节中,我们将为网关服务添加一个过滤器,实现将当前用户 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 也是工作正常良好,并将路由请求转发到了认证服务上。

本小节源码下载

https://t.zsxq.com/F34yQ