目录
前言
当把一个单体架构拆分成不同的微服务时,由于每个微服务都有不同的地址或者端口。当请求不同数据时要访问不同的入口,需要维护多个入口地址,麻烦。
同时单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取用户信息。而在微服务拆分之后,每个微服务都要独立部署,这就存在一些问题:每个微服务都要做一次登录校验吗?微服务之间相互调用的时候,该如何传递用户信息?
因此上述问题都需要网关来进行解决。
一、网关路由
1. 认识网关
网关就是网络的关口。数据在网络之间传输,从一个网络传输到另一个网络的时候就需要经过网关来做数据的路由和转发以及数据安全的校验。
微服务网关就可以起到相同的作用。前端请求不能直接访问微服务,而是要先请求网关:
网关可以做安全控制,也就是登录身份校验,校验通过才放行
通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
下面我们使用SpringCloudGateway来实现网关技术,它是基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强。
2. 快速入门
网关本身也是一个微服务,因此也需要创建一个模块开进行功能的开发。大概步骤如下:
创建网关微服务
引入SpringCloudGateway、NacosDiscovery依赖
编写启动类
配置网关路由
a) 首先创建一个新的maven项目,命名为hm-gateway,作为网关微服务:
b) 引入依赖
<!--网关-->
<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>
</dependencies>
c) 配置启动类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
d) 配置路由
路由信息要在yaml文件中进行配置,
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101: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/**
完成上述配置,既可以通过访问8080端口来对其他的微服务进行访问。例如:http://localhost:8080/items/page?pageNo=1&pageSize=1
二、网关登录校验
1.思路分析
登录基于JWT来实现,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:
每个微服务都需要知道JWT的秘钥,不安全
每个微服务重复编写登录校验代码、权限校验代码,麻烦
因此可以把登录校验的工作放到网关来做
2. 网关过滤器
登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是网关内部代码实现的,要想在请求转发之前做登录校验,就必须了解内部工作的基本原理。
客户端请求进入网关后由
HandlerMapping
对请求做判断,找到与当前请求匹配的路由规则(Route
),然后将请求交给WebHandler
去处理。WebHandler
则会加载当前路由下需要执行的过滤器链(Filter chain
),然后按照顺序逐一执行过滤器。图中
Filter
被分为左右两部分,是因为Filter
内部的逻辑分为pre
和post
两部分,分别会在请求路由到微服务之前和之后被执行。只有所有
Filter
的pre
逻辑都依次顺序执行通过后,请求才会被路由到微服务。微服务返回结果后,再倒序执行
Filter
的post
逻辑。最终把响应结果返回。
因此我们需要自定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter
之前,就可以实现对用户的登录校验了。
3. 自定义过滤器
使用GlobalFilter来实现过滤器,以下代码只是一个框架,如果过滤器逻辑通过则放行;如果没有通过,那么就直接拦截,不会向下继续执行。
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑(过滤器逻辑)
。。。。。
// 传递用户信息,将用户信息保存在请求头当中
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
// 放行
return chain.filter(swe);
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
4. 微服务获取用户信息
在上面的代码中网关将解析出的用户ID添加到了新的请求头当中,请求头中有了一个新的参数叫做:user-info,保存的就是登录用户的信息。
然后微服务通过Interceptor拦截器从请求头中提取信息,并通过ThreadLocal来进行存储(拦截器可以定义在一个Common模块中,然后所有的微服务来引用该模块的依赖即可)。
5. OpenFeign传递用户
前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。
但是有些微服务之间也要相互传递用户信息。比如下单业务,流程如下:
下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!
由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。
微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?
Feign中提供了一个拦截器接口:RequestInterceptor。
我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate
类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。
在这里使用匿名内部类的形式来实现:
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}