文章目录
- 前言
- 一、网关路由
- 二、SpringCloudGateway
- 1. 路由过滤
- 2. 网关登录校验
- 2.1 鉴权
- 2.2 网关过滤器
- 2.3 登录校验
- 2.3.1 JWT
- 2.3.2 登录校验过滤器
- 3. 微服务从网关获取用户
- 4. 微服务之间用户信息传递
- 三、nacos配置管理
- 问题引入
- 3.1 配置共享
- 3.1.1 在Nacos中添加共享配置
- 3.1.2 拉取共享配置
- 3.2 配置热更新
- 问题引入
- 实现步骤
- 3.3 动态路由
- 问题引入
- 3.3.1 监听nacos配置变更,更新路由表
- 3.3.2 实现动态路由
前言
前端请求不能直接访问微服务,而是要请求网关(SpringCloudGateway)
网关干什么?路由过滤,登录校验。
nacos既是注册中心也可用作配置管理,用来解决各个微服务块配置文件中相同的配置冗余,配置热更新属性,动态路由等。
一、网关路由
数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。
微服务中的网关作用:前端请求不能直接访问微服务,而是要请求网关。
- 网关可以做安全控制,也就是登录身份校验,校验通过才放行
- 通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
二、SpringCloudGateway
网关本身也是一个独立的微服务,所以也需要单独建立一个模块。实现步骤:
- 创建网关微服务模块
- 引入SpringCloudGateway、NacosDiscovery依赖
- 编写启动类
- 配置网关路由
1. 路由过滤
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/** #请求路径必须符合指定规则
RouteDefinition就是具体的路由规则定义。
- id:路由的唯一标示
- predicates:路由断言,其实就是匹配条件
- filters:路由过滤条件
- uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。
2. 网关登录校验
2.1 鉴权
在网关中保存密钥开发登录校验功能
问题
1、怎么在转发之前做登录校验
2、校验JWT之后,怎么把用户信息传递给微服务
3、微服务之间怎么传递用户信息
2.2 网关过滤器
实现原理
- 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
- WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
- 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为pre和post两部分,分别会在请求路由到微服务之前和之后被执行。
- 只有所有Filter的pre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
- 微服务返回结果后,再倒序执行Filter的post逻辑。
- 最终把响应结果返回。
定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前?
网关过滤器
- GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
- GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。FilteringWebHandler在处理请求时,会将GlobalFilter装饰为GatewayFilter,然后放到同一个过滤器链中,排序以后依次执行。
- AddRequestHeaderGatewayFilterFacotry: 就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。直接在yaml文件中配置
1、自定义GatewayFilter
定义一个类,这个类的名称一定要以GatewayFilterFactory为后缀,这个类继承了AbstractGatewayFilterFactory
2、自定义GlobalFilter
定义一个类,直接实现GlobalFilter接口。
2.3 登录校验
2.3.1 JWT
- AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
- JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置
- SecurityConfig:工具的自动装配
- JwtTool:JWT工具,其中包含了校验和解析token的功能
- hmall.jks:秘钥文件
2.3.2 登录校验过滤器
定义一个登录校验的过滤器,实现GlobalFilter接口。
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtTool jwtTool;
private final AuthProperties authProperties;
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
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
token = headers.get(0);
}
// 4.校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果无效,拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
// TODO 5.如果有效,传递用户信息
//保存用户请求头
String userInfo = userId.toString();
ServerWebExchange ex = exchange.mutate()
.request(b -> b.header("user-info",userInfo))
.build();
// 6.放行
return chain.filter(exchange);
}
private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
3. 微服务从网关获取用户
将用户信息以请求头的方式传递到下游微服务,然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。
实现步骤:
- 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
- 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行(因为每个微服务都执行获取用户信息的逻辑,因此统一拦截所有请求,进行处理(AOP的思想))
4. 微服务之间用户信息传递
由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。
微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?
Feign中提供的一个拦截器接口:feign.RequestInterceptor
实现方式
定义一个类,实现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());
}
};
}
三、nacos配置管理
问题引入
- 网关路由在配置文件中写死了,如果变更必须重启微服务
- 某些业务配置在配置文件中写死了,每次修改都要重启服务
- 每个微服务都有很多重复的配置,维护成本高
nacos: 无需重启即可生效,实现配置热更新。实现动态路由功能,无需重启网关即可修改路由配置。
3.1 配置共享
3.1.1 在Nacos中添加共享配置
抽取配置文件yaml中重复的模块,如jdbc相关配置(datasource、mybatis-plus)、日志配置(logging)、swagger(knife4j)以及OpenFeign的配置(feign)
3.1.2 拉取共享配置
如何去加载nacos中的配置文件?
实现步骤
1、在模块中引入依赖
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
2、新建bootstrap.yaml,配置nacos地址
spring:
cloud:
nacos:
server-addr: 192.168.150.101 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享日志配置
3.2 配置热更新
问题引入
业务相关参数,将来可能会根据实际情况临时调整。例如购物车业务,购物车数量有一个上限,默认是10。
实现步骤
1、在nacos中添加配置属性
2、新建一个属性读取类,在需要使用该属性的业务类中注入。
3.3 动态路由
问题引入
网关的路由配置全部是在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变,也不会监听路由变更。
- 如何监听Nacos配置变更?
- 如何把路由信息更新到路由表?
3.3.1 监听nacos配置变更,更新路由表
使用 Nacos 动态监听配置接口来实现。
public void addListener(String dataId, String group, Listener listener)
核心点:
- 创建ConfigService,连接到Nacos(spring-cloud-starter-alibaba-nacos-config自动装配,拿到NacosConfigManager就等于拿到了ConfigService)
- 添加配置监听器,编写配置变更的通知处理逻辑
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.创建ConfigService,连接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.读取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置监听器
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
// 配置变更的通知处理
System.out.println("recieve1:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
更新路由表
package org.springframework.cloud.gateway.route;
import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public interface RouteDefinitionWriter {
/**
* 更新路由到路由表,如果路由id重复,则会覆盖旧的路由
*/
Mono<Void> save(Mono<RouteDefinition> route);
/**
* 根据路由id删除某个路由
*/
Mono<Void> delete(Mono<String> routeId);
}
3.3.2 实现动态路由
1、网关gateway引入依赖
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加载bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
2、在网关gateway的resources目录创建bootstrap.yaml文件
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101
config:
file-extension: yaml
shared-configs:
- dataId: shared-log.yaml # 共享日志配置
3、在gateway中定义配置监听器
package com.hmall.gateway.route;
import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.hmall.common.utils.CollUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {
private final RouteDefinitionWriter writer;
private final NacosConfigManager nacosConfigManager;
// 路由配置文件的id和分组
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
// 保存更新过的路由id
private final Set<String> routeIds = new HashSet<>();
@PostConstruct
public void initRouteConfigListener() throws NacosException {
// 1.注册监听器并首次拉取配置
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
updateConfigInfo(configInfo);
}
});
// 2.首次启动时,更新一次配置
updateConfigInfo(configInfo);
}
private void updateConfigInfo(String configInfo) {
log.debug("监听到路由配置变更,{}", configInfo);
// 1.反序列化
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2.更新前先清空旧路由
// 2.1.清除旧路由
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
// 2.2.判断是否有新的路由要更新
if (CollUtils.isEmpty(routeDefinitions)) {
// 无新路由配置,直接结束
return;
}
// 3.更新路由
routeDefinitions.forEach(routeDefinition -> {
// 3.1.更新路由
writer.save(Mono.just(routeDefinition)).subscribe();
// 3.2.记录路由id,方便将来删除
routeIds.add(routeDefinition.getId());
});
}
}
4、在nacos中配置路由信息,路由文件名为gateway-routes.json,类型为json
[
{
"id": "item",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}],
"filters": [],
"uri": "lb://item-service"
},
....