1.网关需要的依赖
<!--网关-->
<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>
2.路由的yaml配置文件举例
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.*.*:8848
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-1路由属性
- id:路由唯一标识
- uri:路由目标地址
- predicates:路由断言,判断请求是否符合当前路由
- filters:路由过滤器,对请求或响应做特殊处理
2-1-1 路由断言
Spring提供了12种基本的RoutePredicateFactory实现:
2-1-2 路由过滤器
3.网关登录校验
以前单体架构,我们只需要在项目中定义一个拦截器,拦截所有请求,对用户身份进行验证。而现在的微服务模式下,这样显然行不通。而网关作为一个微服务群的入口,就担当了这一责任。网关的作用是做路由转发,那我们的登录校验操作是不是得在做路由转发之前来做呢?没错,我们应该了解一下网关路由转发的流程。
如图,首先请求会经过路由映射器,通过路由断言找到匹配的路由,存入上下文,并把请求交给请求处理器,这个处理器会加载我们配置的多个过滤器,形成过滤器链,依次执行。过滤器分为两个阶段,一个是pre阶段,一个是post阶段,前者是之前后者是之后。如图,请求如果被过滤了,那么后续过滤器不在处理,最后一个过滤器是Netty路由过滤器,它的作用就是把请求转发到微服务中。由此,如果要做登录校验,我们就要自己定义一个过滤器,在Netty之前。Netty路由过滤器是自动就有的。把用户信息放在请求的请求头中。
3-1 自定义过滤器
- GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效。要配置路由后才生效。就是刚才33种过滤器。
- GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效。
public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
- ServerWebExchange exchange :是请求上下文,包含整个过滤器链内共享数据,例如request,response等
- GatewayFilterChain chain:过滤器链。当前过滤器执行完后,要调用过滤器链中下一个过滤器
我们自定义过滤器,首先要比NettyRoutingFilter的优先级要高
可以看到NettyRoutingFilter实现了GlobalFilter与Ordered接口,Ordered接口就是指定排序的
可以看到Ordered接口的getOrder方法返回的int类型的最大值21亿多,于是我们自己定义的过滤器应该实现GlobalFilter与Ordered接口,getOrder方法返回值应该小于这个int的最大值。如下,举个例子。
简单示例1:
@Component
public class MyFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 模拟登录校验逻辑
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
System.out.println("headers = " + headers);
//放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
具体示例2:
@RequiredArgsConstructor //通过构造方法变成bean对象
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtTool jwtTool; //自定义JWT工具类,含有构建和解析jwt的方法
private final AuthProperties authProperties; //属性类,加载了配置文件中的exclude路径集合
private final AntPathMatcher antPathMatcher = new AntPathMatcher(); //特殊的模式匹配,主要匹配路径是否符合标准
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取request
ServerHttpRequest request = exchange.getRequest();
//2.判断是否需要做拦截操作
if (isExclude(request.getPath().toString())) {
return chain.filter(exchange); //如果是排除的路径,那就放行
}
//3.拿到请求头,取出token,健壮性判断
List<String> tokens = request.getHeaders().get(HttpHeaders.AUTHORIZATION);
String token = null;
if (tokens != null && !tokens.isEmpty()) {
token = tokens.get(0);
}
Long userId = null;
//4.解析token
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED); //如果解析失败,给响应设置状态码为401,即为授权失败
return response.setComplete(); //后续过滤器不会再走,直接结束,把响应给前端
}
//5.拿到用户信息
String userInfo = userId.toString();
ServerWebExchange exc = exchange.mutate() //mutate就是对下文请求做修改
.request(builder -> builder.header("user-info", userInfo))
.build();
//6. 放行
return chain.filter(exc);
}
private boolean isExclude(String path) {
for (String excludePath : authProperties.getExcludePaths()) {
// 如果传过来的path满足模式匹配,说明这个路径是排除的路径,直接放行就可
if (antPathMatcher.match(excludePath, path)) return true;
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
3-2 拦截器来存放用户信息
在做日常开发中,我们的微服务几乎一定会用到用户id来查询某些信息,之前我们在网关中,把用户id(userId)放进了请求头,我们可以使用SpringMVC的拦截器来把userId存入ThreadLocal中,但是每一个微服务都要用到这个userId,总不能在每一个微服务中都写一个mvc的拦截器吧。于是我们可以把这个拦截器写入一个公共的项目中,也就是每一个微服务都引入了这个项目的坐标。一般这种项目名称叫common-service。但是一般情况,来做网关的这个微服务也会引入这个公共项目,但是网关它和mvc是两个完全不在一个层面的东西,如果也引入,那一定会报错。所以,在拦截器中,就需要有条件的做拦截动作,而mvc它一定有一个公共的类的使用,那就是DispatcherServlet.class
// 这是自定义拦截器
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 这个拦截器不做拦截,只做存取用户信息的工具,一律放行
// 从请求头中取出用户id
String userInfo = request.getHeader("user-info");
// 健壮性判断
if (StrUtil.isNotBlank(userInfo)) {
// 存入 ThreadLocal
UserContext.setUser(Long.valueOf(userInfo));
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理用户
UserContext.removeUser();
}
}
//这是mvc的配置
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
而且还需要在静态资源下的META-INF下的spring.factories,放入MvcConfig的包路径,以成为ioc容器内的bean
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MvcConfig
3-3 openfeign的传递用户信息
现在我们解决了网关到微服务之间的用户信息传递,但是微服务彼此之间的用户信息传递又该怎么办呢?拦截器是在mvc中进行的,如果我们从一个服务请求另一个服务,那么这个请求就是服务自己发的,不是前端发的,这个请求是一个新的请求,请求头中是没有用户信息(user-info)的,拦截器是在controller层前做拦截把用户信息放入ThreadLocal的,所以我们的这个请求就需要把本次从前端来的请求头中的用户信息再放入这个服务发起的请求中。
openfeign提供了一个接口,如下
public interface RequestInterceptor {
void apply(RequestTemplate var1);
}
我们需要实现这个接口,来做把用户信息放入请求头中这个操作,这里我采用匿名内部类的方式
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate request) {
Long userId = UserContext.getUser();
if(userId != null){
request.header("user-info", userId.toString());
}
}
};
}
注意!!!如果要让这个类生效,一定要把它加在openfeign的启动类上@EnableFeignClients
@EnableFeignClients(basePackages = "***.***.api.client", defaultConfiguration = DefaultFeignConfig.class)
这里用到了很多拦截器,如图