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就可以直接具备拦截器功能,无需重复编写。

相关推荐
zhangxueyi4 分钟前
Tomcat与Nginx之全面比较
linux·运维·服务器·nginx·tomcat
尘佑不尘12 分钟前
linux命令详解,openssl+历史命令详解
linux·运维·服务器·笔记·web安全
飞大圣2 小时前
Ubuntu 18 EDK2 环境编译
linux·运维·ubuntu
福如意如我心意2 小时前
Nginx 的 proxy_pass 使用简介
运维·nginx·proxy_pass
星辰@Sea2 小时前
Nginx 部署负载均衡服务全解析
运维·nginx·负载均衡
冷心笑看丽美人2 小时前
RHEL 网络配置(Linux网络服务器 09)
linux·运维·服务器·网络·bash
运维&陈同学2 小时前
【HAProxy06】企业级反向代理HAProxy调度算法之其他算法
运维·nginx·云计算·负载均衡·lvs·haproxy·反向代理
运维&陈同学2 小时前
【HAProxy05】企业级反向代理HAProxy调度算法之静态算法与动态算法
linux·运维·算法·nginx·云原生·负载均衡·lvs·haproxy
默闻革2 小时前
Nginx报错unknown directive “ssl“
运维·nginx·ssl
关李屁氏2 小时前
如何编写jenkins的流水线
运维·jenkins