专栏目录
0.docker快速入门
1.初识微服务
2.Gateway网关路由
3.微服务配置管理
网关就是网络的关口。数据从一个网络传输到另一网络时,通过网关来实现数据的路由和转发以及数据安全的校验。
在微服务中,前端请求不能直接访问到服务,因此需要通过网关的路由转发来实现请求,前端只需要知道网关的地址就好
- 网关可以做安全控制,也就是登录身份校验,校验通过才放行
- 通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
- 网关也是一种微服务,可以通过在注册中心拉取到已注册的微服务的相关信息
Spring Cloud中网关的实现有两种方案:
①网关属于一种微服务,因此需要单独的项目模块
②引入相关依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
dependency>
③编写启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
④配置路由转发规则
spring:
application:
name: gateway
cloud:
nacos:
server-addr: ip:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://{服务名称} # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://{服务名称}
predicates:
- Path=/carts/**
定义语法:
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://{服务名称}
predicates:
- Path=/items/**,/search/**
网关路由对应Java的
RouteDefinition
id:路由唯一标识
uri:路由目标地址
lb://
代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问
predicates:路由断言,判断是否符合当前路由
filters:路由过滤器,对请求或响应进行处理
详情请见RoutePredicate
名称 | 说明 | 示例 |
---|---|---|
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=/path_1/{segment},/path_2/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的IP必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
weight | 权重处理 |
过滤器内部包含两部分逻辑,分为pre和post,分别在请求路由到微服务之前和之后执行
当所有Filter的pre都执行通过后,请求才会被路由到微服务上,否则会被拦截,后续过滤器不再执行
微服务返回结果后倒叙执行Filter的post逻辑
详情请见GatewayFilter
名称 | 说明 | 示例- filters: |
---|---|---|
AddRequestHeader | 给当前请求添加一个请求头 | -AddRequestHeader=headerName,headerValue |
RemoveRequestHeader | 移除请求中的一个请求头 | -RemoveRequestHeader=headerName |
AddResponseHeader | 给响应结果中添加一个响应头 | -AddResponseHeader=headerName,headerValue |
RemoveResponseHeader | 从响应结果中移除有一个响应头 | -RemoveResponseHeader=headerName |
RewritePath | 请求路径重写 | -RewritePath=/red/?(? /${segment} |
StripPrefix | 去除请求路径中N段前缀 | -StripPrefix=1(如/a/b路径只保留/b) |
示例:
spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
filters:
- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
spring:
cloud:
gateway:
default-filters: # default-filters下的过滤器可以作用于所有路由
- AddRequestHeader=key, value
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
①创建自定义过滤器的类,类名命名规则为**GatewayFilterFactory
,继承实现AbstractGatewayFilterFactory
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 编写过滤器逻辑
System.out.println("过滤器执行了");
// 放行
return chain.filter(exchange);
}
};
}
}
②在配置yaml配置文件
spring:
cloud:
gateway:
default-filters:
- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
直接创建自定义的类,继承实现GlobalFilter
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
//ServerWebExchange是请求上下文,包含整个过滤器链内的共享数据
//GatewayFilterChain是过滤器链,指向下一个过滤器
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
System.out.println("未登录,无法访问");
// 放行
// return chain.filter(exchange);
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验。
但如果每个微服务都去校验,就会造成安全隐患(每个微服务都需要知道JWT的秘钥),并且影响开发效率,代码冗余(每个微服务重复编写登录校验代码、权限校验代码)
因此可以使用网关实现登录的校验,即只需要在网关和用户服务保存秘钥,并开发登录校验功能
在网关中自定义全局过滤器,位于pre部分且位于过滤器链的首端,功能是JWT校验并保存到请求头
@Component
public class UserInfoGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取请求
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();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("headerName"),userInfo)
.build();
// 6.放行
return chain.filter(swe);
}
// 过滤器执行顺序,值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
①在微服务中定义一个SpringMVC拦截器在common中,拦截用户信息并保存到ThreadLocal中
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("headerName");
// 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();
}
}
②配置SpringMVC拦截器
import com.hmall.common.interceptor.UserInfoInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
③SpringBoot自动装配,~/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.project.common.config.MyBatisConfig,\
com.project.common.config.MvcConfig
Gateway是基于WebFlux的响应式,不包含SpringMVC,因此不能令MVC拦截器在网关中生效
//DispathcerServlet.Class是MVC中的一个类,因此可以作为自动装配的条件
@Configuration
@ConditionalOnClass(DispathcerServlet.Class)
public class MvcConfig implements WebMvcConfigurer{...}
某些业务可能需要多个微服务协同完成,因此应该实现用户信息在多个服务之间的相互传递
使用OpenFeign中提供的拦截器接口(RequestInterceptor
)实现微服务之间的信息传递,所有由OpenFeign发起的请求都会先调用拦截器拦截请求
public interface RequestInterceptor {
/**
* Called for every request.
* Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}
定义拦截器在api模块中
@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());
}
};
}