SpringCloudGateway — 网关登录校验

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。

1. 思路分析

既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做。

不过,这里存在几个问题:

(1)网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?

可以使用网关过滤器。

(2)网关校验JWT之后,如何将用户信息传递给微服务?

由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。

(3)微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?

在微服务发起调用时把用户信息存入请求头。

2. 登录校验过滤器

白名单的配置:

application.ymlapplication.properties 文件中配置不需要拦截的路径白名单。

复制代码
gateway:
  ignoreUrls:
    - /auth/login
    - /auth/register
    - /public/**

在这个示例中,/auth/login/auth/register/public/** 等路径将不需要进行登录校验。

java 复制代码
package com.cyt.gateway.filter;

@Component // 声明为Spring容器中的一个Bean
@RequiredArgsConstructor // 生成构造方法,注入final成员变量
@EnableConfigurationProperties(AuthProperties.class) // 启用配置类AuthProperties的属性注入
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    // 从配置文件中注入白名单路径列表,用于存放不需要JWT校验的路径
    @Value("${gateway.ignoreUrls}")
    private List<String> ignoreUrls;

    private final JwtTool jwtTool; // JWT工具类,用于解析和校验JWT

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 获取Request对象,以访问请求的相关信息
        ServerHttpRequest request = exchange.getRequest();

        // 2. 判断请求路径是否在白名单中(无需JWT校验)
        if (isIgnoreUrl(request.getPath().toString())) {
            // 路径在白名单中,直接放行请求
            return chain.filter(exchange);
        }

        // 3. 获取请求头中的token(假设token放在"authorization"头部)
        String token = null;
        List<String> headers = request.getHeaders().get("authorization");
        if (!CollUtils.isEmpty(headers)) { // 如果请求头包含authorization字段
            token = headers.get(0); // 获取第一个token值
        }

        // 4. 使用jwtTool校验并解析token
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token); // 解析并获取用户ID
        } catch (UnauthorizedException e) {
            // 如果token无效或解析失败,则返回401状态码并拦截请求
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(401);
            return response.setComplete(); // 返回空的响应结束处理
        }

        // TODO 5. 如果token有效,可在此处传递用户信息(如通过exchange.getAttributes())
        System.out.println("userId = " + userId); // 打印用户ID供调试

        // 6. 放行请求
        return chain.filter(exchange);
    }

    /**
     * 判断路径是否在白名单中
     *
     * @param path 请求路径
     * @return 如果路径在白名单中返回true,否则返回false
     */
    private boolean isIgnoreUrl(String path) {
        for (String ignoreUrl : ignoreUrls) {
            // 使用正则匹配白名单路径中的通配符 "**"
            if (path.matches(ignoreUrl.replace("**", ".*"))) {
                return true; // 路径匹配白名单中的某一项,返回true
            }
        }
        return false; // 没有匹配到任何白名单路径,返回false
    }

    @Override
    public int getOrder() {
        return 0; // 设置过滤器优先级,值越小优先级越高
    }
}

3. 保存用户到请求头

5.6.部分代码可以参照下面修改:

java 复制代码
// 5.传递用户信息
// 将解析出的用户ID转换为字符串,以便可以在请求头中传递
String userInfo = userId.toString();

// 使用 exchange.mutate() 方法创建一个新的 ServerWebExchange 对象,
// 该对象将携带额外的请求头 "user-info",包含用户ID信息。
// .mutate() 方法用于复制当前请求并进行修改。
// 在这里,我们使用 request(builder -> builder.header(...)) 方式为请求添加一个新的头部。
ServerWebExchange modifiedExchange = exchange.mutate()
        .request(builder -> builder.header("user-info", userInfo)) // 将 "user-info" 头设置为用户ID字符串
        .build(); // 完成新的 ServerWebExchange 对象的构建

// 6.放行
// 使用修改后的 ServerWebExchange 对象继续执行过滤链。
// 这样,下游服务可以通过 "user-info" 请求头获取到用户ID信息。
return chain.filter(modifiedExchange);

4. OpenFeign传递用户

在微服务发起调用时把用户信息存入请求头。

如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?

这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor

我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。

可以在OpenFeign的配置类中添加一个Bean:

java 复制代码
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
    // 定义一个 Feign 的 RequestInterceptor Bean,用于在请求发出前执行自定义拦截操作
    return new RequestInterceptor() {
        @Override
        public void apply(RequestTemplate template) {
            // 获取当前登录用户的ID
            // UserContext 是一个上下文工具类,用于存储和获取当前线程的用户信息
            Long userId = UserContext.getUser();
            
            // 检查用户ID是否为 null,如果为 null 则表示用户未登录或无法获取用户信息
            if(userId == null) {
                // 如果用户ID为空,不做任何处理,直接返回,跳过拦截器逻辑
                return;
            }

            // 如果用户ID不为空,将用户ID作为请求头添加到 Feign 请求中
            // "user-info" 是请求头的键,下游微服务可以从该请求头中获取用户ID
            template.header("user-info", userId.toString());
        }
    };
}

5. 总结

思路分析中提到的三个问题,目前已经全部解决。

接下来,微服务需要用户信息,只需要编写拦截器,获取用户信息并保存到ThreadLocal中,然后放行即可。

由于每个微服务都有获取登录用户的需求,因此拦截器可以直接写在common公共服务中,并写好自动装配。这样微服务只需要引入common就可以直接具备拦截器功能,无需重复编写。

相关推荐
七夜zippoe6 分钟前
CANN Runtime任务描述序列化与持久化源码深度解码
大数据·运维·服务器·cann
Fcy6482 小时前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满2 小时前
Linux怎么查看最新下载的文件
linux·运维·服务器
代码游侠2 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法
Harvey9032 小时前
通过 Helm 部署 Nginx 应用的完整标准化步骤
linux·运维·nginx·k8s
珠海西格电力科技3 小时前
微电网能量平衡理论的实现条件在不同场景下有哪些差异?
运维·服务器·网络·人工智能·云计算·智慧城市
释怀不想释怀3 小时前
Linux环境变量
linux·运维·服务器
zzzsde4 小时前
【Linux】进程(4):进程优先级&&调度队列
linux·运维·服务器
聆风吟º5 小时前
CANN开源项目实战指南:使用oam-tools构建自动化故障诊断与运维可观测性体系
运维·开源·自动化·cann
NPE~5 小时前
自动化工具Drissonpage 保姆级教程(含xpath语法)
运维·后端·爬虫·自动化·网络爬虫·xpath·浏览器自动化