上一篇:SpringCloud 入门(3)—— Nacos配置中心-CSDN博客
Spring Cloud Gateway 作为 Spring Cloud 生态系统的一部分,主要在微服务架构中充当 API 网关的角色。它提供了统一的入口点来处理所有的 HTTP 请求,并将这些请求路由到不同的后端微服务。
Spring Cloud Gateway 是一个强大的工具,它不仅简化了微服务间的通信,还增强了系统的安全性、稳定性和可扩展性。对于任何采用微服务架构的应用程序来说,它都是不可或缺的一部分。
1.快速入门
1.1创建网关
单独创建一个模块,配置网关
首先导入网关所需依赖
<!--网关-->
<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>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
本地服务中配置,指定nacos地址
#微服务配置
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 124.70.208.223:8848
discovery:
namespace: dev
group: xuecheng-plus-project # Nacos 服务发现的组名,用于进一步隔离服务
config:
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true #动态刷新
shared-configs:
- data-id: logging-${spring.profiles.active}.yaml
refresh: true
profiles:
active: dev
1.2nacos配置中心
在nacos配置中心中进行远程配置;所有的网关相关配置全部配置到nacos中,创建gateway-dev.yaml
server:
port: 63010 # 网关端口
spring:
cloud:
gateway:
routes: # 网关路由配置
- id: content-api # 路由id,自定义,只要唯一即可
uri: lb://content-api # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/content/** # 这个是按照路径匹配,只要以/content/开头就符合要求
- id: system-api
uri: lb://system-api
predicates:
- Path=/system/**
- id: media-api
uri: lb://media-api
predicates:
- Path=/media/**
四个属性含义如下:
-
id
:路由的唯一标识 -
uri
:路由目标地址,lb://
代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。 -
predicates
:路由断言,其实就是匹配条件 -
filters
:路由过滤条件,
1.3路由匹配条件
SpringCloudGateway中支持的断言类型有很多:
名称 | 说明 | 示例 |
---|---|---|
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 | 权重处理 |
2.网关全局过滤器
在网关处,在filter包中实现GlobalFilter接口配置全局过滤器,实现 Ordered接口定义过滤器的优先级。
在过滤器中,首先获取需要放行的路径(在配置文件中定义,AuthProperties 类中读取不进行拦截的路径),如果请求路径,不需要拦截,直接放行。
如果该路径在配置文件中没有定义,都需要拦截,首先获取请求头中的authorization,拿到token值,进行解析。拿到用户的id。将用户的id放到请求头中user-info,转发给其他的微服务
package com.hmall.gateway.filter;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@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();
}
// 5.如果有效,传递用户信息
String userInfo = userId.toString();
System.out.println("userId"+userInfo);
ServerWebExchange swe = exchange.mutate().request(builder -> builder.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;//数字约小,优先级越高
}
}
上面我们已经在网关过滤器中,获取请求头中的token,解析出用户id,放回请求头user-info中,传递给各个微服务。
但通过openFeign发起的请求中,并没有通过网关,请求中没有携带user-info。
所以我们需要配置openFeign拦截器,拿到用户id,并放到请求头中,转发给其他微服务
下面是针对openFeign的配置
package com.hmall.api.config;
import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){//日志信息
return Logger.Level.FULL;
}
@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());
}
};
}
}
配置完成后需要在使用了openFeign微服务的启动类上加上@EnableFeignClients注解,激活拦截器
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
@MapperScan("com.hmall.pay.mapper")
@SpringBootApplication
public class PayApplication {
public static void main(String[] args) {
SpringApplication.run(PayApplication.class, args);
}
}
3.配置全局id
3.1SpringMvc拦截器
在网关和OpenFeign中我们已经将用户id放到了请求头user-info中,而所有的微服务,都需要依赖常量模块common,故我们在common模块中,定义SpringMVC拦截器,获取userId,保存到TreadLocal中。
这里可能有人会问,为什么不在网关过滤器中直接将用户id保存在TreadLocal中呢?,而是放到请求头中,在common模块中,定义拦截器拿到用户id,在保存TreadLocal中。这就不得不提一下TreadLocal的特性了
ThreadLocal
是 Java 中的一个工具,它为每个线程提供独立的变量副本,使得同一个线程在不同地方访问ThreadLocal
变量时都能得到相同的数据,而不同线程之间不会相互影响。然而,ThreadLocal
的作用范围仅限于当前 JVM(Java 虚拟机)中的线程,并且它的生命周期与线程的生命周期绑定。在微服务架构中,网关和各个微服务通常是独立部署的应用程序,它们可能运行在不同的进程中,甚至是在不同的服务器上。因此,当你在一个微服务(如网关)中使用
ThreadLocal
来保存用户信息时,这些信息不会自动传递到其他微服务中。
在common模块中定义拦截器
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的用户信息
String userInfo = request.getHeader("user-info");
// 2.判断是否为空
if (StrUtil.isNotBlank(userInfo)) {
// 不为空,保存到ThreadLocal
UserContext.setUser(Long.valueOf(userInfo));
}
// 3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserContext.removeUser();
}
}
添加拦截器
import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是common模块,因为common模块中的扫描包路径与其它微服务的扫描包不一致,且常量模块中,没有单独的SpringBootApplication启动类,无法被扫描到,因此无法生效。
基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
注解: @ConditionalOnClass(DispatcherServlet.class)
DispatcherServlet
是 Spring MVC 的核心组件,负责接收和分发 HTTP 请求到相应的控制器。因此,当应用程序是基于 Spring MVC 构建时,类路径中必然包含 DispatcherServlet
类
@ConditionalOnClass
用于自动配置类中,以确保该拦截器只有在处理http请求时才生效,拦截。
3.2TreadLocal
package com.hmall.common.utils;
public class UserContext {
private static final ThreadLocal<Long> tl = new ThreadLocal<>();
/**
* 保存当前登录用户信息到ThreadLocal
* @param userId 用户id
*/
public static void setUser(Long userId) {
tl.set(userId);
}
/**
* 获取当前登录用户信息
* @return 用户id
*/
public static Long getUser() {
return tl.get();
}
/**
* 移除当前登录用户信息
*/
public static void removeUser(){
tl.remove();
}
}