网关基础知识
1.网关路由
网关:就是网络的关口,负责请求的路由、转发、身份校验。
在SpringCloud中网关的实现包括两种:
1.Spring Cloud Gateway
Spring官方出品
基于WebFlux响应式编程
无需调优即可获得优异性能
2.Netflix Zuul
Netflix出品
基于Servlet的阻塞式编程
需要调优才能获得与SpringCloudGateway类似的性能
1.基础使用
1.创建新的模块作为网关的模块。
2.导入网关的依赖:(要用到网关以及nacos)
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
3.在模块下创建启动类
4.配置路由规则
server:
port: 8080 #网关对应的端口
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.145.129:8848 #nacos服务注册
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**
2.路由属性
网关路由对应的Java类型是RouteDefinition,其中常见的属性有:
lid:路由唯一标示
luri:路由目标地址
lpredicates:路由断言,判断请求是否符合当前路由。
lfilters:路由过滤器,对请求或响应做特殊处理。
2.网关登录校验
问题:请求发到微服务的每个项目中,不可能每一个项目都做登录校验,因此要在网关统一进行登录校验。
1.自定义过滤器
过滤器执行是有顺序的,执行pre过程后,请求访问完在进行post过程。
网关过滤器有两种,分别是:
GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效。
GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效。
GlobalFilter使用:
自定义GlobalFilter直接实现GlobalFilter即可,而且无法设置动态参数:
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
System.out.println("请求头是"+headers);
// 放行
// return chain.filter(exchange);
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
GatewayFilter使用:
自定义GatewayFilter
不是直接实现GatewayFilter
,而是实现AbstractGatewayFilterFactory
。最简单的方式是这样的:
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 编写过滤器逻辑
System.out.println("过滤器执行了");
// 放行
return chain.filter(exchange);
}
};
}
}
注意:该类的名称一定要以GatewayFilterFactory
为后缀!
然后在yaml配置中这样使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:
@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 编写过滤器逻辑
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// 放行
return chain.filter(exchange);
}
}, 100);
}
// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
static class Config{
private String a;
private String b;
private String c;
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
// 返回当前配置类的类型,也就是内部的Config
@Override
public Class<Config> getConfigClass() {
return Config.class;
}
}
然后在yaml文件中使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制
上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值。
还有一种用法,无需按照这个顺序,就是手动指定参数名:
spring:
cloud:
gateway:
default-filters:
- name: PrintAny
args: # 手动指定参数名,无需按照参数顺序
a: 1
b: 2
c: 3
2.实现登录校验
整体思路:微服务的登录校验要在网关统一做拦截处理,在网关中解析jwt令牌判断请求能否转发并得到userid,利用exchange.mutate()函数将userid添加到请求头中转发到微服务中,在common模块中设置拦截器在请求头中获取userid并存入线程中,如此便可在项目中用userid查询数据。要在微服务中传递userid,可以用feign.RequestInterceptor
实现将userid添加到请求头中发送给另一个微服务,此微服务解析出userid即可。
登录校验过滤器
@Component
@RequiredArgsConstructor
public class AutoGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
//匹配器,匹配路径是否相符
private final AntPathMatcher antPathMatcher=new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求头
ServerHttpRequest request = exchange.getRequest();
//判断是否要拦截
if (isexclude(request.getPath().toString())) {
//放行
return chain.filter(exchange);
}
System.out.println("判断之后");
//获取token
String token = null;
Long userid = null;
List<String> list = request.getHeaders().get("authorization");
if (list != null && !list.isEmpty()) {
token = list.get(0);
}
//解析token
try {
userid = jwtTool.parseToken(token);
} catch (Exception e) {
//拦截,设置响应码为401
ServerHttpResponse response = exchange.getResponse();
//两种方式设置响应码为401
//response.setRawStatusCode(401);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//传递用户信息
String useridString = userid.toString();
ServerWebExchange serverWebExchange = exchange.mutate()
.request(builder -> builder.header("user-info", useridString))
.build();
//放行
return chain.filter(serverWebExchange);
}
private boolean isexclude(String path) {
for (String excludePath : authProperties.getExcludePaths()) {
if (antPathMatcher.match(excludePath,path)){
System.out.println("路径放行"+excludePath);
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
AntPathMatcher是spring提供的一个类,可以检验路径是否匹配
3.网关传递用户信息
1.在登录校验中获取到token中的userid,添加到发送到微服务中的请求头中
String useridString = userid.toString();
ServerWebExchange serverWebExchange = exchange.mutate()
.request(builder -> builder.header("user-info", useridString))
.build();
2.在微服务中接收userid
在controller中获取userid,代码如下:
@ApiOperation("查询购物车列表")
@GetMapping
public List<CartVO> queryMyCarts(@RequestHeader(value = "user-info",required = false)String userinfo){
System.out.println("用户id是"+userinfo);
return cartService.queryMyCarts();
}
问题:要在每个cintroller中获取,太繁琐,因此要在拦截器中统一获取
在拦截器中获取userid
网关从token中获取到userd后,把它存入到了请求头中,当微服务接收到请求头后,解析出userid,存入线程之中,之后项目即可在线程中获取到userid。
public class interceptors implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取用户登录信息
String userinfo = request.getHeader("user-info");
//判断是否获取了信息,若获取则存入线程
if (StrUtil.isNotBlank(userinfo)){
UserContext.setUser(Long.valueOf(userinfo));
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清理用户
UserContext.removeUser();
}
}
ps:
1.因为此代码编写在common中,微服务引用的common包,因此作为配置类的拦截器无法被微服务spring扫描到。解决办法是在resources目录下创建文件META-INF/spring.factories,如图
在此文件下编写如下代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.webinterceptorconfig,\
com.hmall.common.config.JsonConfig
当别的项目引用common模块时这些配置类即可配别的项目扫描并自动装配
2.要让common在网关不生效,在微服务生效,在注册拦截器的配置类上添加如下注解:
@ConditionalOnClass(DispatcherServlet.class)
@ConditionalOnClass标识在@Configuration类上,只有存在@ConditionalOnClass中value/name配置的类该Configuration类才会生效,而微服务是mvc框架,网关不是,因此上述代码可以解决问题。
4.openfeign传递用户信息
在openfeign中添加userid到请求头,要借助Feign中提供的一个拦截器接口:
feign.RequestInterceptor,
实现apply方法,利用RequestTemplate
类来添加请求头,从线程中获
取userid,将用户信息保存到请求头中。代码如下:
@Bean
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userid = UserContext.getUser();
if (userid!=null){
requestTemplate.header("user-info",userid.toString());
}
}
};
}
保留问题:RequestInterceptor
怎么作用于特定的openfeign请求?