网关
纵向拆分 (Vertical Splitting / Business Domain Splitting): 关注点是业务功能完整性。每个服务是一个独立的业务单元,例如:用户服务、商品服务、订单服务、支付服务。它们通常有自己的数据库。
水平拆分 (Horizontal Splitting / Cross-cutting Concern Splitting): 关注点是通用能力或技术功能。这些功能被多个业务服务共享,例如:认证授权服务、日志服务、配置服务、消息队列服务、文件存储服务。它们通常是无状态的或有自己的独立数据存储。
API 网关 (API Gateway): 它是微服务架构中的一个关键组件,扮演着门面 (Facade) 的角色。它不仅处理横切关注点(如认证、限流),更重要的是提供统一的入口和路由,将内部复杂的微服务结构对外部客户端透明化,从而简化客户端与微服务系统的交互。
SpringCloudGateway
相比于Netfilx的Zuul,SpringCloudGateway更好用,也更受欢迎
导入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <dependencies> <dependency> <groupId>com.heima</groupId> <artifactId>hm-common</artifactId> <version>1.0.0</version> </dependency> <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> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
|
配置路由规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| server: port: 8080 spring: application: name: gateway cloud: nacos: server-addr: 127.0.0.1:8848 gateway: routes: - id: item-service uri: lb://item-service predicates: - Path=/items/**,/search/** ……
|
路由断言
Spring提供了多种基本的RoutePredicateFactory
路由断言的模板使用方法
| 名称 |
说明 |
| After |
是某个时间点后的请求 |
| Before |
是某个时间点之前的请求 |
| Between |
是某两个时间点之前的请求 |
| Cookie |
请求必须包含某些cookie |
| Header |
请求必须包含某些header |
| Host |
请求必须是访问某个host (域名) |
| Method |
请求方式必须是指定方式 |
| Path |
请求路径必须符合指定规则 |
| Query |
请求参数必须包含指定参数 |
| RemoteAddr |
请求者的ip必须是指定范围 |
| Weight |
权重处理 |
| XForwarded Remote Addr |
基于请求的来源IP做判断 |
路由过滤器
路由过滤器有更多中配置的方式,在此处我们列出常用的路由配置
同样的,我们也可以去官网找到配置说明和样板代码
| 名称 |
说明 |
| AddRequestHeader |
给当前请求添加一个请求头 |
| RemoveRequestHeader |
移除请求中的一个请求头 |
| AddResponseHeader |
给响应结果中添加一个响应头 |
| RemoveResponseHeader |
从响应结果中移除一个响应头 |
| RewritePath |
请求路径重写 |
| StripPrefix |
去除请求路径中的N段前缀 |
如果想要该过滤器对所有的路由都生效,则需要配置默认过滤器
1 2 3 4 5
| gateway: routes: …… default-filters: - AddResponseHeader= truth, try
|
自定义过滤器
自定义过滤器也分为普通过滤器和全局过滤器
在这里,普通过滤器自定义起来很难,也很少有应用场景,所以大多使用全局过滤器
GlobalFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); return chain.filter(exchange); }
@Override public int getOrder() { return 0; } }
|
登陆校验
在网关处自定义一个全局过滤器,获取请求头中的path信息,比对排除地址,看是否需要校验token
从请求头中拿到token,调用JwtTool解析token,拿到userId放到请求头中向下传递并放行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
|
@Component @RequiredArgsConstructor public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override public int getOrder() { return 0; }
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest();
if (isExcludePath(request.getPath().toString())) { return chain.filter(exchange); }
String token = null; List<String> authorization = request.getHeaders().get("Authorization"); if (authorization != null && !authorization.isEmpty()) { token = authorization.get(0); }
Long userId = null; try { userId = jwtTool.parseToken(token); } catch (UnauthorizedException e) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); }
return chain.filter(exchange); }
private boolean isExcludePath(String path) { for (String pathPattern : authProperties.getExcludePaths()){ if (antPathMatcher.match(pathPattern, path)) { return true; } } return false; } }
|
UserId
在微服务中,我们可以设置MVC的过滤器,拿到请求头的userId并存放到ThreadLocal中
设置请求头
1 2 3 4 5 6
| String userInfo = userId.toString(); ServerWebExchange swe = exchange.mutate().request( builder -> builder .header("user-info", userInfo) ).build();
|
获取请求头
每一个微服务都需要使用拦截器获取到请求头中的userId,所以我们将获取逻辑抽取到Common库中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class UserInfoInterceptor implements HandlerInterceptor { @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContext.removeUser(); }
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String UserInfo = request.getHeader("User-Info"); if (StrUtil.isBlank(UserInfo)) { UserContext.setUser(Long.valueOf(UserInfo)); } return true; } }
|
别忘了要在common中新建一个配置类
1 2 3 4 5 6 7 8
| @Configuration @ConditionalOnClass(DispatcherServlet.class) public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInfoInterceptor()); } }
|
spring.factories
在 Spring Boot 微服务架构中,当 common 模块作为共享库被其他微服务依赖时,common 模块中定义的配置类在微服务启动时不会被自动装配。
Spring Boot 默认的组件扫描机制仅会扫描主应用类所在的包及其子包。如果 common 模块的配置类位于不同的根包下,或不在主应用类的扫描路径内,它们将无法被 Spring Boot 容器发现和加载。
为了使 common 模块的配置类能够被引用它的微服务自动加载,我们应利用 Spring Boot 的自动配置机制
在 common 模块的 src/main/resources/META-INF/ 目录下创建 spring.factories 文件
1 2 3 4
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hmall.common.config.MyBatisConfig,\ com.hmall.common.config.MvcConfig,\ com.hmall.common.config.JsonConfig
|
OpenFeign传递UserInfo
OpenFeign服务间调用并不会走Mvc,也就不会走Mvc的过滤器(也就是提取UserInfo)
那么在服务间调用应该如何传递UserInfo呢?
OpenFeign中提供了拦截器的接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Bean public RequestInterceptor requestInterceptor() { return new RequestInterceptor(){ @Override public void apply(feign.RequestTemplate requestTemplate) { Long userid = UserContext.getUser(); if (userid != null) { requestTemplate.header("user-id", userid.toString()); } } }; }
|
别忘了在启动类的注释中应用这个拦截器
1
| @EnableFeignClients(basePackages = "com.hmall.api.client",defaultConfiguration = FeignInterceptor.class)
|