网关及配置管理

网关及配置管理

  • 在单体项目中,前端只需要请求某一个地址的端口方可请求到数据
  • 但在微服务架构中,前端请求的端口可能不是同一个,功能各不相同,端口也不同,甚至在部署时地址端口都不同
  • 用户jwt校验,在微服务中由于只有用户微服务具有jwt校验功能,其他微服务无法调用,

什么是网关

定义: 网络的关口,负责请求的路由、转发、身份校验

网关的实现方式

springCloud中网关的主要实现方式有:

  • SpringCloud Gateway:Spring官方出品 基于webFlux响应式编程 (无需调优即可获得优异性能)
  • Zuul:Netflix出品 基于Servlet阻塞式编程 (需要调优才能获得与SpringCloud Gateway相应性能)

在此项目中我们将使用SpringCloud Gateway网关 新建网关微服务

路由规则

spring:
  cloud:
    gateway:
      routes: #路由规则可以有多个 每一个前面用 - 隔开
        - id: user_route #路由规则id,自定义,唯一
          uri: lb://user-service #路由目标微服务 lb: 表示使用负载均衡 //后加微服务名称
          predicates: #路由断言,判断请求是否符合规则,符合则路由到目标 可以有多个规则用 - 隔开
            - Path=/user/**,/item /** # [规则名称]=路由规则 多个用逗号隔开

        - id: item_route #路由规则id,自定义,唯一
          uri: lb://item-service #路由目标微服务 lb: 表示使用负载均衡
          predicates: #路由断言,判断请求是否符合规则,符合则路由到目标 可以有多个规则用 - 隔开
            - Path=/item/**

网关微服务配置

server:
  port: 8080 # 设置网关端口 前端请求优先请求网关 由网关进行路由转发
# 路由规则
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: ${hm.nacos.host}:${hm.nacos.port} #配置注册中心地址
    gateway:
      routes:
        - id: item-service
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/** #多个使用逗号隔开

        - id: cart-service
          uri: lb://cart-service
          predicates:
            - Path=/carts/**

路由属性

网关路由对应的java类型是RouteDefinition,其中常见的属性有:

  • id: 路由id,自定义,唯一 列如user_route
  • uri: 路由目标微服务地址 列如lb://user-service lb:为负载均衡
  • predicates: 路由断言,判断请求是否符合规则,符合则路由到目标 可以有多个规则用 , 隔开 列如Path=/user/**,/item/**
  • filters: 路由过滤器,对请求或相应做特殊处理 可以有多个规则用 , 隔开 列如AddResponseHeader=X-Anonymous, true

路由断言

Spring提供了12种基本的RoutePredicateFactory实现

名称说明示例
After是指某个时间点之后的请求- After=2017-01-20T17:42:47.879-07:00[America/Denver]
Before是指某个时间点之前的请求- Before=2017-01-20T17:42:47.879-07:00[America/Denver]
Between某个时间段内的请求- Between=2017-01-20T17:42:47.879-07:00[America/Denver],2017-01-21T17:42:47.879-07:00[America/Denver]
Cookie带有指定cookie的请求- Cookie=chocolate, ch.p
Header带有指定header的请求- Header=X-Request-Id, \d+
Host带有指定host的请求- Host=**.zalando.com
Method带有指定method的请求- Method=GET
Path带有指定path的请求- Path=/user/**,/item/**
Query带有指定query的请求- Query=error, true
RemoteAddr请求的ip必须是指定范围- RemoteAddr=192.168.1.1/24
Weight指定权重的请求- Weight=group1, 5
X-Forwarded-Prefix基于请求的来源ip做判断- X-Forwarded-Prefix=/api

路由过滤器

名称说明示例
AddRequestHeader给当前请求添加请求头- AddrequestHeader=headerName, headerValue
RemoveRequestHeader删除请求头- RemoveResponseHeader=headerName
AddResponseHeader给当前请求添加响应头- AddResponseHeader=headerName, headerValue
RemoveResponseHeader删除响应头- RemoveResponseHeader=headerName
RewritePath重写请求路径- RewritePath=/foo/(?<segment>.*), /$\{segment}
StripPrefix删除请求路径中的N段前缀- StripPrefix=1 ,则/a/b转发时只保留/b

网关登录校验

网关登录校验共有三个问题

  • 如何在网关转发之前做登录校验
  • 网关如何将用户信息传递给微服务
  • 如何在微服务之间传递用户信息

自定义网关过滤器

网关过滤器有两种,分别是:

  • GatewayFilter :路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效
  • GlobalFilter :全局过滤器,作用于所有路由;声明后自动生效

全局过滤器

package com.hmall.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    //ServerWebExchange : 请求上下文 包含了整个过滤器链的共享信息 例如:request和 response
    //GatewayFilterChain : 过滤器链 当过滤器执行完后,要调用过滤器链中的下一个过滤器
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("全局过滤器");
        System.out.println("header" + exchange.getRequest().getHeaders().toString());
        //调用过滤器链继续执行
        return chain.filter(exchange);
    }

    //实现Ordered接口,设置优先级,数字越小优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}

无参路由过滤器

  • 路由过滤器:需要配置才可生效
  • 配置值为:过滤器配置类GatewayFilterFactory前面的字符作为过滤器名称
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: ${hm.nacos.host}:${hm.nacos.port} #配置注册中心地址
    gateway:
      default-filters:
        - PrintAny #过滤器配置类  GatewayFilterFactory前面名称
package com.hmall.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
//class名称 必须以GatewayFilterFactory 结尾
//GatewayFilterFactory前面的字符用于过滤器名称配置
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    @Override
    public GatewayFilter apply(Object config) {
        //OrderedGatewayFilter 参数为 GatewayFilter和int int 表示优先级
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("自定义路由过滤器");
                return chain.filter(exchange);
            }
        }, 1);
    }
}

带参路由过滤器

default-filters:
  - PrintAny=1,2,3
package com.hmall.gateway.filter;

import lombok.Data;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@Component
//class名称 必须以GatewayFilterFactory 结尾
//GatewayFilterFactory前面的字符用于过滤器名称配置
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
    public PrintAnyGatewayFilterFactory() {
        //将config字节码传递给父类,父类负责读取yml配置
        super(Config.class);
    }

    @Data
    //自定义配置属性
    public static class Config {
        private String a;
        private String b;
        private String c;
    }

    //将变量名称依次返回 顺序很重要 将来读取参数时需要按照顺序读取
    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("a", "b", "c");
    }


    @Override
    public GatewayFilter apply(PrintAnyGatewayFilterFactory.Config config) {
        //OrderedGatewayFilter 参数为 GatewayFilter和int int 表示优先级
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("自定义路由过滤器" + config);//自定义路由过滤器PrintAnyGatewayFilterFactory.Config(a=1, b=2, c=3)
                return chain.filter(exchange);
            }
        }, 1);
    }


}

用户登录校验

package com.hmall.gateway.filter;

import cn.hutool.core.text.AntPathMatcher;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import io.netty.util.internal.StringUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    private final JwtTool jwtTool;
    private final AuthProperties authProperties;
    //AntPathMatcher 路径匹配器
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("全局过滤器");
        //判断是否需要登录校验
        if (isExclude(exchange.getRequest().getPath().toString())) return chain.filter(exchange);
        //获取请求头中的 token
        String token = exchange.getRequest().getHeaders().getFirst("authorization");
        if (StringUtil.isNullOrEmpty(token)) return unauthorized(exchange);
        // 2.校验token
        Long userId = jwtTool.parseToken(token);
        if (userId == null) return unauthorized(exchange);
        // 3.传递用户信息
        System.out.println("用户id" + userId);
        return chain.filter(exchange);
    }

    private boolean isExclude(String string) {
        //检查至少有一个元素满足条件
        return authProperties.getExcludePaths().stream().anyMatch(path -> antPathMatcher.match(path, string));
    }

    @Override
    public int getOrder() {
        return 0;
    }

    //返回未登录请求状态码
    private Mono<Void> unauthorized(ServerWebExchange exchange) {
        //获取请求返回
        ServerHttpResponse response = exchange.getResponse();
        //设置未登录请求状态码
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //返回数据
        return response.setComplete();
    }
}

网关传递用户信息到微服务

在网关微服务中做登录校验

原理:将网关解析获取到的用户信息封装到请求的请求头中 改变下游的请求头

 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    System.out.println("全局过滤器");
    //判断是否需要登录校验
    if (isExclude(exchange.getRequest().getPath().toString())) return chain.filter(exchange);
    //获取请求头中的 token
    String token = exchange.getRequest().getHeaders().getFirst("authorization");
    if (StringUtil.isNullOrEmpty(token)) return unauthorized(exchange);
    // 2.校验token
    Long userId = jwtTool.parseToken(token);
    if (userId == null) return unauthorized(exchange);
    // 3.传递用户信息
    System.out.println("用户id" + userId);
    //构建包含用户信息请求头的请求
    ServerWebExchange user = exchange.mutate().request(builder -> builder.header("user", userId.toString())).build();
    //将新的请求像下游传递
    return chain.filter(user);
}

在公共的微服务中获取用户信息

由于我们每个微服务都引用了公共的hm-common模块,因此可以在hm-common模块中定义一个拦截器用户获取用户信息

既然公共模块可以获取用户信息,那么为什么不直接在公共模块中解析token直接获取用户信息呢

职责分离原则
  • 网关层应该负责统一的身份验证和授权检查
  • 微服务专注于业务逻辑,而不是安全认证逻辑
  • 这样做符合单一职责原则
安全性考虑
  • 如果在hm-common中解析token意味着每个微服务都有能力处理JWT
  • 网关作为入口统一处理认证更加安全,可以集中管理和控制访问策略
  • 避免各个微服务重复实现相同的认证逻辑,减少出错可能性
灵活性和可维护性
  • 认证逻辑集中在网关,当需要修改认证方式时只需改动网关
  • 不同微服务可能有不同的权限需求,网关可以统一处理后再传递必要信息
  • 便于实现更复杂的认证策略,如OAuth2API密钥等
性能优化
  • 网关可以缓存认证结果,避免每个微服务都重复解析token
  • 减少微服务间的网络通信开销

实操

定义拦截器
package com.hmall.common.interceptors;

import com.hmall.common.utils.UserContext;
import io.netty.util.internal.StringUtil;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserInfoInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取登录用户信息
        String headers = request.getHeader("user-id");
        //判断是否获取了用户,如果有再存入ThreadLocal
        if (!StringUtil.isNullOrEmpty(headers)) return true;
        //获取到了用户信息 存入ThreadLocal
        UserContext.setUser(Long.parseLong(headers));
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //请求完毕清理用户
        UserContext.removeUser();
    }
}

注册拦截器
package com.hmall.common.config;

import com.hmall.common.interceptors.UserInfoInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    final UserInfoInterceptor userInfoInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //加入拦截器
        registry.addInterceptor(userInfoInterceptor);
    }
}

加入配置类扫描
  • 由于我们的拦截器是放在hm-common模块中的,其他微服务的模块并不能直接扫描到该配置类,因此需要加入配置类扫描
  • 因此需要在hm-common模块中resources文件夹中新建META-INF/spring.factories文件手动加入扫描
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
解决网关依赖冲突
  • 由于拦截器是依赖spring-webmvc的,网关依赖了hm-common那么网关中也会自动装配,用于执行拦截器
  • 但网关不需要执行拦截器,并且网关中的Spring Cloud Gateway(响应式应用)与Servlet相关依赖冲突
  • 因此不在网关中执行拦截器
  • 在拦截器配置类中加入条件化装配注解

@Configuration
//条件化装配 只有在特定类型的Web应用程序环境中才会激活被注解的配置类或组件
//type属性:指定Web应用类型
//ConditionalOnWebApplication.Type.SERVLET:仅在基于Servlet的Web应用中激活(如传统的Spring MVC应用)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //加入拦截器
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

OpenFeign传递用户信息

  • OpenFeign提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器的处理请求
  • 其中提供的RequestInterceptor接口在RequestTemplate对象中就可以操作请求头
package com.hmall.api.config;

import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;

public class DefaultFeginConfig {
    @Bean //将此方法的返回值作为bean加入到spring容器中
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public RequestInterceptor userInfoInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    requestTemplate.header("user-id", userId.toString());
                }
            }
        };
    }
}

上次更新 2025/12/23 21:15:18