微服务—Gateway
微服务—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: xxuri: lb://xx-servicepredicates:— 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-gatewaycloud:gateway:routes:- id: springcloud-alibaba-consumer #设置iduri: lb://springcloud-alibaba-consumer #设置服务名predicates:- Method=GET,POST #设置请求方法- Path=/apia/** #设置匹配路径的网关filters:- StripPrefix=1 #去掉前缀offer- id: springcloud-alibaba-feignconsumeruri: lb://springcloud-alibaba-feignconsumerpredicates:- Method=GET,POST- Path=/apib/**filters:- StripPrefix=1
server:port: 8080logging: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 {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//模拟登录校验逻辑ServerHttpRequest request = exchange.getRequest(); //获取请求HttpHeaders headers = request.getHeaders();//获取请求头System.out.println("headers = " + headers);//放行return chain.filter(exchange);}@Overridepublic 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_serviceuri: lb://order-servicepredicates:- Path=/order/**,/shopping/**,/cart/** # 以请求路径做判断,以 /order , /shopping , /cart 开头 则符合规则default-filters: # 全局过滤器 拦截所有请求- printAny=小新,5,男
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {@Overridepublic GatewayFilter apply(Config config) {return new OrderedGatewayFilter( new GatewayFilter(){@Overridepublic 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);}// 自定义配置属性 , 成员变量名称很重要@Datapublic static class Config{private String name;private int age;private String sex;}@Overridepublic 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: Haikouname: Xxxage: 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 {@Autowiredprivate AuthYaml authYaml;//将 JwtTool 工具类注入@Autowiredprivate JwtTool jwtTool;private final AntPathMatcher antPathMatcher = new AntPathMatcher();// 路径匹配器 spring 内置的 路径匹配器@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//1、获取 requestServerHttpRequest request = exchange.getRequest();//2、判断是否需要做登录拦截//判断请求路径,是否和我们在yaml文件中白名单配置的路径一致,如果一致就放行RequestPath path = request.getPath();//通过,我们自定义的 isTrue 方法判断是否需要做登录拦截if (this.isTrue(path.toString())) {//放行return chain.filter(exchange);}//下面是需要进行登录拦截校验的逻辑//3、获取tokenList<String> auth = request.getHeaders().get("authorization");String token = null;if (auth != null && !auth.isEmpty()){System.out.println("auth = " + auth);token = auth.get(0);}//4、校验并解析 tokentry {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文件中白名单配置的路径一致,如果一致就放行,返回truefor (String excludePath : authYaml.getExcludePaths()) {if(antPathMatcher.match(excludePath, path)){// 匹配成功return true;}}return false;}// 优先级 值越小,优先级越高//保证我们的过滤器在 NettyRoutingFilter 之前执行@Overridepublic int getOrder() {return 1;}
}
-
AuthYaml
相关xjh:jwt:location: HaiKoualias: xjhpassword: 123456tokenTTL: 30mauth: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、校验并解析 jwtJWT 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 { //生成密钥的配置类@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic 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: HaiKoualias: xjhpassword: 123456tokenTTL: 30mauth: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 {@Autowiredprivate AuthYaml authYaml;//将 JwtTool 工具类注入@Autowiredprivate JwtTool jwtTool;private final AntPathMatcher antPathMatcher = new AntPathMatcher();// 路径匹配器 spring 内置的 路径匹配器@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//1、获取 requestServerHttpRequest request = exchange.getRequest();//2、判断是否需要做登录拦截//判断请求路径,是否和我们在yaml文件中白名单配置的路径一致,如果一致就放行RequestPath path = request.getPath();//通过,我们自定义的 isTrue 方法判断是否需要做登录拦截if (this.isTrue(path.toString())) {//放行return chain.filter(exchange);}//下面是需要进行登录拦截校验的逻辑//3、获取tokenList<String> auth = request.getHeaders().get("authorization");String token = null;if (auth != null && !auth.isEmpty()){System.out.println("auth = " + auth);token = auth.get(0);}//4、校验并解析 tokenLong 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文件中白名单配置的路径一致,如果一致就放行,返回truefor (String excludePath : authYaml.getExcludePaths()) {if(antPathMatcher.match(excludePath, path)){// 匹配成功return true;}}return false;}// 优先级 值越小,优先级越高//保证我们的过滤器在 NettyRoutingFilter 之前执行@Overridepublic 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的拦截器还需要进行配置才可以@Overridepublic 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 {@Beanpublic RequestInterceptor userInfoRequestInterceptor() {return new RequestInterceptor() {@Overridepublic 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 {@Beanpublic RequestInterceptor userInfoRequestInterceptor() {return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 获取当前登录用户信息Long userId = UserContext.getUserId();if (userId != null) {// 将用户信息添加到请求头中template.header("user-info", String.valueOf(userId));}}};}}
重要!!!!
想要上面的配置类生效,我们需要把这个配置类,加在 feign 微服务的启动类之上
@EnableFeignClients(defaultConfiguration =FeignConfig.class )