网关及配置管理
网关及配置管理
- 在单体项目中,前端只需要请求某一个地址的端口方可请求到数据
- 但在微服务架构中,前端请求的端口可能不是同一个,功能各不相同,端口也不同,甚至在部署时地址端口都不同
- 用户
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_routeuri: 路由目标微服务地址 列如lb://user-servicelb:为负载均衡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 - 网关作为入口统一处理认证更加安全,可以集中管理和控制访问策略
- 避免各个微服务重复实现相同的认证逻辑,减少出错可能性
灵活性和可维护性
- 认证逻辑集中在网关,当需要修改认证方式时只需改动网关
- 不同微服务可能有不同的权限需求,网关可以统一处理后再传递必要信息
- 便于实现更复杂的认证策略,如
OAuth2、API密钥等
性能优化
- 网关可以缓存认证结果,避免每个微服务都重复解析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());
}
}
};
}
}