微服务—Gateway
参考项目路径: D:\Users\lenovo\Desktop\Java学习-代码集\Myself_Practice
网关:就是网络关口,负责请求的路由,转发、身份校验。
配置路由规则
spring:
cloud:
gateway:
routes:
— id: item # 路由规则id , 自定义 唯一 (最好和微服务名一致)
uri: lb://item-service # 路由目标微服务,lb 代表负载均衡
predicates: # 路由断言,判断请求是否符合规则,符合规则到路由
— Path=/items/** # 以请求路径做判断,以 /items 开头 则符合规则
— id: xx
uri: lb://xx-service
predicates:
— Path=/xx/**,/XX/**,/xx/**
- id:为每条路由规则设定的唯一标识符,建议和微服务名称保持一致。
- uri:代表路由目标微服务,lb:// 表示使用负载均衡。
- predicates:属于路由断言,借助特定条件来判定请求是否符合规则,符合的话就进行路由。
- Path=/items/** 表示请求路径以 /items 开头的请求会被路由到 item-service 微服务。
依赖
<!-- gateway 应用禁止引入spring-boot-starter-web 依赖,
如果引入,当前应用无法启动!!!!!-->
<!--geateway 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<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>
快速使用
一:新建一个模块,创建一个网关服务
二:新建一个 yaml 文件,配置 网关路由规则
示例:
spring:
application:
name: springcloud-alibaba-gateway
cloud:
gateway:
routes:
- id: springcloud-alibaba-consumer #设置id
uri: lb://springcloud-alibaba-consumer #设置服务名
predicates:
- Method=GET,POST #设置请求方法
- Path=/apia/** #设置匹配路径的网关
filters:
- StripPrefix=1 #去掉前缀offer
- id: springcloud-alibaba-feignconsumer
uri: lb://springcloud-alibaba-feignconsumer
predicates:
- Method=GET,POST
- Path=/apib/**
filters:
- StripPrefix=1
server:
port: 8080
logging:
level:
org.springframework.cloud.gateway: debug
这时,我们通过8080 端口,通过路由匹配规则之后,就可以访问各个模块的接口 (注意,使用的是服务名发现,所以我们要在启动类上面加上 @EnableDiscoveryClient
这个注解)
路由属性
网关路由对应的 Java 类型 是 RouteDefinition
, 其中常见的属性有:
- id: 路由唯一标识
- uri: 路由目标地址
- predicates: 路由断言,判断请求是否符合当前路由
- filters: 路由过滤器,对请求或响应做特殊处理
路由断言 predicates:
Spring 提供了 12种基本的 RoutePredicateFactory
实现
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | -After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | -Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | -Between=2037-01-20T17:42:47.789-07:00[America/Denver],2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | -Cookie=chocolate,ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id,\d+ |
Host | 请求必须是访问某个host(域名) | -Host=**.somehost.org,**.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | -Path=/red/{segment),/blue/** |
Query | 请求参数必须包含指定参数 | -Query=name,Jack 或者-Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | -RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 | -Weight=group1,2 |
XForwarded Remote Addr | 基于请求的来源IP做判断 | -XForwardedRemoteAddr=192.168.1.1/24 |
路由过滤器 filter :
参考 Spring Cloud Gateway 官方文档
网关登录校验
思路: 在 网关里面做 JWT 校验(转发之前),响应之后,将登录用户信息传给后面的服务。
网关请求处理流程:
大致流程: 网关拦截判断客户端发送的请求,然后,进行路由匹配,再转发到对应的微服务模块中去
零:引出问题:
- 如何在网关转发之前做登录校验?
- 网关如何将用户信息传递给微服务?
- 如何在微服务之间传递用户信息?
所以我们要将 登录校验,放在 pre 阶段,也就是 过滤器之前。因此我们需要在网关内自定义一个过滤器,保证这个过滤器的执行顺序,在 NettyRoutingFilter
之前,并且还要保证在 pre 逻辑里,另外网关还需要将用户信息传递给各个微服务(保存用户到请求头)
一:自定义过滤器:
网关过滤器有两种,分别是:
- GatewayFilter:路由过滤器,作用于任意指定得路由,默认不生效,要配置到路由后生效。
- GlobalFilter:全局过滤器,作用氛围是所有路由;声明后自动生效。
public interface GlobalFilter {
/**
* ServerWebExchange: 请求上下文 包含整个过滤器链内共享数据,例如 request response等
* GatewayFilterChain: 过滤器链 当前过滤器执行完之后,要调用过滤器链的下一个过滤器
* @param exchange
* @param chain
* @return
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
自定义 GlobalFilter
示例:
@Component
public class MyGlobalFilter 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() {
//保证我们的过滤器在 NettyRoutingFilter 之前执行
//实现Ordered 接口,返回值越小,优先级越高 NettyRoutingFilter的返回值最大所以我们return 0 就可以在它前面执行
return 0;
}
}
我们实现 GlobalFilter
接口,声明这个类是全局拦截器,并通过 @Component
加到容器当中,然后,获取请求,进行一些登录校验的逻辑
我们还要确保这个自定义的全局拦截器要在 NettyRoutingFilter
拦截器之前运行,所以,实现 Ordered
接口,并返回0 数值越小,优先级越高。
自定义 GatewayFilter
自定义 GatewayFilter 不是直接实现 GatewayFilter , 而是继承 AbstractGatewayFilterFactory
spring:
cloud:
gateway:
routes:
- id: user_service # 路由规则id , 自定义 唯一
uri: lb://user-service # 路由目标微服务,lb 代表负载均衡
predicates: # 路由断言,判断请求是否符合规则,符合规则到路由
- Path=/user/** # 以请求路径做判断,以 /user 开头 则符合规则
- id: post_service
uri: lb://order-service
predicates:
- Path=/order/**,/shopping/**,/cart/** # 以请求路径做判断,以 /order , /shopping , /cart 开头 则符合规则
default-filters: # 全局过滤器 拦截所有请求
- printAny=小新,5,男
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter( new GatewayFilter(){
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String name = config.getName();
System.out.println("name = " + name);
//模拟登录校验逻辑
ServerHttpRequest request = exchange.getRequest();//获取请求
HttpHeaders headers = request.getHeaders();//获取请求头
System.out.println("打印日志");
//放行 让下一个过滤器执行
return chain.filter(exchange);
}
}, 1);
}
// 自定义配置属性 , 成员变量名称很重要
@Data
public static class Config{
private String name;
private int age;
private String sex;
}
@Override
public List<String> shortcutFieldOrder() {
//将 name age sex 顺序和配置文件保持一致
List<String> list = new ArrayList<>();
list.add("name");
list.add("age");
list.add("sex");
return list;
}
// 构造器 将 config 字节码传递给父类, 父类负责帮我们读取 yaml 的配置
public PrintAnyGatewayFilterFactory(){
super(Config.class);
}
}
首先要注意的是,我们自定义的
GatewayFilter
的类名要是统一的后缀为GatewayFilterFactory
,例如PrintAnyGatewayFilterFactory
这个是自定义了一个有参数的
GatewayFilter
拦截器 我们在yaml
文件中 写参数。另外 我们的
GatewayFilter
工厂 new 的是OrderedGatewayFilter
方便我们进行拦截器的先后拦截顺序。变量的顺序,就和我们在配置文件中的顺序是一致的。
二:实现登录校验:
需求:在网关中基于 过滤器 实现 登录校验 功能
@ConfigurationProperties
是 Spring Boot 框架中的一个注解,它主要用于将配置文件(如 application.properties
或 application.yml
)中的属性值绑定到 Java Bean 上,方便在代码中使用配置信息。
示例:
hm: jwt: location: Haikou name: Xxx age: 16
@Data @Component @ConfigurationProperties(prefix="hm.jwt") public class JwtProperties{ private String location; private String name; private int age; }
-
- 必须为每个需要绑定的字段提供 setter 方法,因为 Spring Boot 通过调用 setter 方法来设置属性值。
- 组件扫描:使用 @
ConfigurationProperties
注解的类需要被 Spring 容器管理,可以使用 @Component 注解或在配置类中使用 @EnableConfigurationProperties
注解来启用。
自定义网关登录校验 过滤器:
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private AuthYaml authYaml;
//将 JwtTool 工具类注入
@Autowired
private JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();// 路径匹配器 spring 内置的 路径匹配器
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1、获取 request
ServerHttpRequest request = exchange.getRequest();
//2、判断是否需要做登录拦截
//判断请求路径,是否和我们在yaml文件中白名单配置的路径一致,如果一致就放行
RequestPath path = request.getPath();
//通过,我们自定义的 isTrue 方法判断是否需要做登录拦截
if (this.isTrue(path.toString())) {
//放行
return chain.filter(exchange);
}
//下面是需要进行登录拦截校验的逻辑
//3、获取token
List<String> auth = request.getHeaders().get("authorization");
String token = null;
if (auth != null && !auth.isEmpty()){
System.out.println("auth = " + auth);
token = auth.get(0);
}
//4、校验并解析 token
try {
Long userId = jwtTool.parseToken(token);
System.out.println("token = " + token);
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 拦截终止请求
}
//5、传递用户信息
//6、放行 让下一个过滤器执行
return chain.filter(exchange);
}
private boolean isTrue(String path) {
// 判断是否需要做登录拦截
//判断和我们在yaml文件中白名单配置的路径一致,如果一致就放行,返回true
for (String excludePath : authYaml.getExcludePaths()) {
if(antPathMatcher.match(excludePath, path)){
// 匹配成功
return true;
}
}
return false;
}
// 优先级 值越小,优先级越高
//保证我们的过滤器在 NettyRoutingFilter 之前执行
@Override
public int getOrder() {
return 1;
}
}
AuthYaml
相关xjh: jwt: location: HaiKou alias: xjh password: 123456 tokenTTL: 30m auth: excludePaths: - /user/login - /user/register
@Data @Component @ConfigurationProperties(prefix = "xjh.auth") public class AuthYaml { private List<String> excludePaths; }
JwtTool
相关@Component public class JwtTool { private final JWTSigner jwtSigner; //正版 public JwtTool(KeyPair keyPair) { this.jwtSigner = JWTSignerUtil.createSigner("HS256", keyPair); } public String createJwt(String userId, Duration ttl) { return JWT.create() .setPayload("userId", userId) .setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis())) // 设置过期时间 .setSigner(jwtSigner) //正版 .sign(); } public Long parseToken(String token) { //1、校验token 是否为空 if (token == null) { throw new UnauthorizedException("token 为空"); } //2、校验并解析 jwt JWT jwt; try { jwt = JWT.of(token).setSigner(jwtSigner); // 正版 } catch (Exception e) { throw new UnauthorizedException("token 解析失败"); } //3、校验 token 是否有效 if (!jwt.verify()) { throw new UnauthorizedException("token 无效"); } //4、校验 token 是否过期 try { JWTValidator.of(jwt).validateDate(); } catch (ValidateException e) { throw new UnauthorizedException("token 过期"); } //5、数据格式校验 Object userId = jwt.getPayload("userId"); if (userId == null) { throw new UnauthorizedException("token 无效"); } //6、数据解析 try { return Long.valueOf(userId.toString()); } catch (NumberFormatException e) { throw new UnauthorizedException("token 无效"); } } }
SecurityConfig
类相关
SecurityConfig
类的主要作用是为 Spring 应用程序配置与安全相关的 Bean。具体来说: 提供一个密码编码器 PasswordEncoder
,用于在用户认证过程中对密码进行加密和验证,保护用户密码的安全性。 从密钥库中获取密钥对 KeyPair
,这个密钥对通常用于 JWT
(JSON Web Token
)的签名和验证,确保 JWT
的完整性和真实性,从而实现基于 JWT
的身份验证和授权机制。
@Configuration // 专门定义配置类 该类可以包含多个 @Bean 注解的方法,这些方法会返回 Spring 容器要管理的 Bean 实例。
@EnableConfigurationProperties(JwtYaml.class)
public class SecurityConfig { //生成密钥的配置类
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public KeyPair keyPair(JwtYaml jwtYaml){
//获取密钥工厂
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(
jwtYaml.getLocation(),
jwtYaml.getPassword().toCharArray());
//获取密钥对
return keyStoreKeyFactory.getKeyPair(
jwtYaml.getAlias(),
jwtYaml.getPassword().toCharArray()
);
}
}
JwtYaml
相关
xjh:
jwt:
location: HaiKou
alias: xjh
password: 123456
tokenTTL: 30m
auth:
excludePaths:
- /user/login
- /user/register
@Data
@Component
@ConfigurationProperties(prefix = "xjh.jwt")
public class JwtYaml {
private Resource location;
private String alias;
private String password;
private Duration tokenTTL;
}
三:网关传递用户:
思路,过滤器经过之后,我们将用户信息保存到请求头当中,然后加一层拦截器,从请求头中拿到信息将用户信息存储到 ThreadLocal
这样就可以不用每个微服务都去进行相关的处理。
3.1 在网关的登录校验过滤器中,把获取到的用户写入请求头
需求:修改 gateway 模块中的 登录校验拦截器,在校验成功之后,将用户 保存到下游请求的请求头当中去。
提示:要修改转发到微服务的请求,需要用到 ServerWebExchange
类下的 API
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private AuthYaml authYaml;
//将 JwtTool 工具类注入
@Autowired
private JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();// 路径匹配器 spring 内置的 路径匹配器
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1、获取 request
ServerHttpRequest request = exchange.getRequest();
//2、判断是否需要做登录拦截
//判断请求路径,是否和我们在yaml文件中白名单配置的路径一致,如果一致就放行
RequestPath path = request.getPath();
//通过,我们自定义的 isTrue 方法判断是否需要做登录拦截
if (this.isTrue(path.toString())) {
//放行
return chain.filter(exchange);
}
//下面是需要进行登录拦截校验的逻辑
//3、获取token
List<String> auth = request.getHeaders().get("authorization");
String token = null;
if (auth != null && !auth.isEmpty()){
System.out.println("auth = " + auth);
token = auth.get(0);
}
//4、校验并解析 token
Long userId;
try {
userId = jwtTool.parseToken(token);
System.out.println("token = " + token);
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 拦截终止请求
}
//5、传递用户信息
String userInfo = String.valueOf(userId);
ServerWebExchange userExchange = exchange.mutate() // 对下游的 request 进行修改 就是修改gateway之后的请求
.request(builder -> builder.header("user-info", userInfo))
.build();
//6、放行 让下一个过滤器执行
return chain.filter(userExchange);
}
private boolean isTrue(String path) {
// 判断是否需要做登录拦截
//判断和我们在yaml文件中白名单配置的路径一致,如果一致就放行,返回true
for (String excludePath : authYaml.getExcludePaths()) {
if(antPathMatcher.match(excludePath, path)){
// 匹配成功
return true;
}
}
return false;
}
// 优先级 值越小,优先级越高
//保证我们的过滤器在 NettyRoutingFilter 之前执行
@Override
public int getOrder() {
return 1;
}
}
对过滤器的第五、六步进行修改
//5、传递用户信息
String userInfo = String.valueOf(userId);
ServerWebExchange userExchange = exchange.mutate() // 对下游的 request 进行修改 就是修改gateway之后的请求
.request(builder -> builder.header("user-info", userInfo))
.build();
//6、放行 让下一个过滤器执行
return chain.filter(userExchange);
mutate
() 就是对下游请求进行修改header(param1 , param2)
里面的两个参数:第一个用户信息在请求头中的名字,第二个就是用户信息- 将 修改之后的 exchange 交到下一个过滤器进行执行
3.2 在common 模块 中编写 SpringMVC
拦截器,获取登录用户 !!!!(重要)
由于可能有多个模块需要获取到用户信息,我们直接在 common 层 定义拦截器,这样只要各微服务模块引用了 common 的依赖,就可以生效,无需重新编写。
//Spring MVC的拦截器还需要进行配置才可以
public class UserInfoInterceptor implements HandlerInterceptor {
/**
*大致思路:
* 在请求到达Controller之前,获取用户信息,然后将用户信息存储到ThreadLocal中。
* 在controller 后,将用户信息从ThreadLocal中移除,避免内存泄漏。
* HandlerInterceptor 拦截器 会在 请求到达Controller之前执行,返回true表示继续执行,返回false表示请求终止。
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取登录用户信息
String userInfo = request.getHeader("user-info"); //这里要和我们在过滤器中设置的一致!!!!!!
//2、判断是否获取到了登录用户信息,有就存储到ThreadLocal中
if (StrUtil.isNotBlank(userInfo)){
//将用户信息存储到ThreadLocal中
UserContext.setUserId(Long.valueOf(userInfo));
}
//3、放行
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清理用户
UserContext.clear();
}
}
写一个
UserContext
工具类,帮助我们进行 用户信息的 管理public class UserContext { private static final ThreadLocal<Long> userId = new ThreadLocal<>(); public static void setUserId(Long id) { userId.set(id); } public static Long getUserId() { return userId.get(); } public static void clear() { userId.remove(); } }
此时,拦截器还不会生效,我们需要进行如下配置
将 SpringMVC
的拦截器 配置到 MVC
当中,利用 addInterceptors
将我们自定义的拦截器添加到 MVC
的配置当中。
@Configuration
//只要是微服务,就有 SpringMVC 就会有DispatcherServlet
//因为网关没有SpringMVC,就没有 DispatcherServlet 所以这个配置在网关服务中就不会生效
@ConditionalOnClass(DispatcherServlet.class) // 仅在存在 DispatcherServlet 类时才加载配置
public class MvcConfig implements WebMvcConfigurer {
//Spring MVC的拦截器还需要进行配置才可以
@Override
public void addInterceptors(InterceptorRegistry registry) {// 拦截器配置
registry.addInterceptor(new UserInfoInterceptor())//添加 UserInfoInterceptor 拦截器
.addPathPatterns("/**"); // 拦截所有请求
}
}
然后在 rescourse
包下创建 META-INF 文件夹, 在文件夹中新建spring.factories
文件
# 这里将 MvcConfig 配置到 spring.factories 文件中
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.common.config.MVCConfig.MvcConfig
此时,我们配置的 webconfig
就可以被扫描到 又因为,各个微服务模块都引用了common 模块,所以说,各个模块的MVC
都可以扫描到,但是,在 Spring Cloud Gateway
里,不存在传统意义上的 Spring MVC
框架,所以可能会扫描不到配置,所以,我们在 MvcConfig
中在配置的时候添加条件
@ConditionalOnClass(DispatcherServlet.class)// 仅在存在 `DispatcherServlet` 类时才加载配置
只要是微服务,就有 SpringMVC
就会有DispatcherServlet
因为网关没有SpringMVC
,就没有 DispatcherServlet
所以这个配置在网关服务中就不会生效
四:OpenFeign
传递用户信息
**需求:**在微服务项目中,很多业务需要多个微服务共同合作完成,而这个过程中也需要传递 登陆用户信息
提示: OpenFeign
中提供了一个拦截器接口,所有由OpenFeign
发起的请求都会先调用拦截器处理请求。
问题: 使用 OpenFeign
调用远程接口时默认不会经过你为普通请求配置的拦截器(如 Spring
MVC
中的 HandlerInterceptor
)
解决方法: 依靠OpenFeign
的拦截接口, RequestInterceptor
接口,其中提供了一些方法可以让我们修改请求头
OpenFeign
在微服务之间互相远程调用接口,而所有的Feign
的远程接口都是定义在api
模块,所以我们应该将拦截接口也定义在 api模块当中。
这里主要是解决 微服务之间通过 OpenFeign
调用的时候,没有经过拦截器的情况,有下面这个配置之后,在远程接口执行之前就会将用户信息添加到请求头之中
public class FeignConfig {
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取当前登录用户信息
Long userId = UserContext.getUserId();
if (userId != null) {
// 将用户信息添加到请求头中
template.header("user-info", String.valueOf(userId));
}
}
};
}
}
重要!!!!
想要上面的配置类生效,我们需要把这个配置类,加在 feign 微服务的启动类之上
@EnableFeignClients(defaultConfiguration =FeignConfig.class )
而所有的Feign
的远程接口都是定义在api
模块,所以我们应该将拦截接口也定义在 api模块当中。**
这里主要是解决 微服务之间通过 OpenFeign
调用的时候,没有经过拦截器的情况,有下面这个配置之后,在远程接口执行之前就会将用户信息添加到请求头之中
public class FeignConfig {
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取当前登录用户信息
Long userId = UserContext.getUserId();
if (userId != null) {
// 将用户信息添加到请求头中
template.header("user-info", String.valueOf(userId));
}
}
};
}
}
重要!!!!
想要上面的配置类生效,我们需要把这个配置类,加在 feign 微服务的启动类之上
@EnableFeignClients(defaultConfiguration =FeignConfig.class )