news 2026/1/2 14:05:55

SpringCould —— 网关详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringCould —— 网关详解

一、前言

在讲网关之前,我们需要先自行拆分黑马商城的其他模块,比如用户微服务、交易微服务、支付微服务。

然后就会发现一个问题,我们在前面的确是可以使用不同端口号对各个微服务进行单一访问的,并且可以用测试文档去测试,这个是没有问题的,但是我们在实际使用时发现,我们是不能通过nginx进行前后端联调的,这是因为nginx的配置只会将前端请求转发到8080,而8080端口并不是我们微服务的端口,所以当我们关闭单体架构项目(单体架构的端口号是8080)时,是肯定无法访问项目的,同时还有一个问题,就是我们无法统一对所有微服务进行管理,我们缺少了一个可以自动转发请求的模块,这个模块就是网关

二、网关路由

1.快速入门

首先要明确一点,网关是怎么知道每个微服务的存在的呢?联想到之前我们也有一个组件可以统一管理端口,就是Nacos,所以网关是从Nacos中拿取微服务的信息(包括微服务注册名)。

那什么是网关路由呢?

网关是所有微服务的路由的集中管理中心,它掌管着所有微服务的路由,当我们的请求从浏览器发送给网关,网关会首先识别这是哪个微服务的路由,然后再把这个请求转发过去,最终交由微服务进行处理,当然,处理的结果(响应)也是需要网关来传回给浏览器的。

一下就是网关的配置文件,首先需要配置网关的端口号,然后要配置Nacos,

最后配置网关路由:

id是可以自己随便取的。

uri是指的对应微服务注册名,注册名前面的lb是指的负载均衡。

predicates断言中配置微服务的路由,也就是拦截这些请求,然后转发给对应配置的微服务。

server: port: 8080 spring: application: name: gateway cloud: nacos: server-addr: 192.168.242.130 gateway: routes: - id: item-service uri: lb://item-service predicates: - Path=/items/**,/search/** - id: user-service uri: lb://user-service predicates: - Path=/users/**,/addresses/** - id: trade-service uri: lb://trade-service predicates: - Path=/orders/** - id: pay-service uri: lb://pay-service predicates: - Path=/pay-orders/** - id: cart-service uri: lb://cart-service predicates: - Path=/carts/**

2.拦截器

网关作为一个管理中心,是可以拿到所有请求信息的,包括请求头请求体等等,并且每次请求都必须通过网关来转发。所以如果我们想统一处理这些请求,在网关中处理是最好的选择。这个可以用于登录校验等具有统一性的处理。

由于业务的不同,我们的拦截器当然需要自己写了,所以这里我们将自己写一个拦截器来尝试模拟登录校验,这是一个全局性的拦截,所以我们实现的接口是GlobalFilter,这是SpringCould提供的接口。

除此之外,我们的网关全局拦截器还需要实现Ordered接口,这个接口是规定拦截器优先级的,网关拦截器的优先级是按照Int的大小进行排序的,优先级最低的是转发请求,所以我们登录校验必须要在转发请求之前校验,自然的,我们的优先级需要比它高,这里我们设置为0即可。

@Component public class MyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //TODO 模拟登录校验逻辑 ServerHttpRequest request = exchange.getRequest(); HttpHeaders headers = request.getHeaders(); System.out.println(headers); //放行 return chain.filter(exchange); } @Override public int getOrder() { return 0; } }

在filter方法中有两个参数,exchange和chain,exchange中包含了很多方法,主要都是用来处理请求信息的,而chain参数可以用来放行,相当于将这一次的请求从该拦截器中放行,请求将移动到拦截器链中的下一个拦截器里去。

这里我们模拟登录校验,就只是打印了一下请求头(因为token是在请求头中的,所以只要能打印出来,就代表拿到了token)。最后放行。

三、项目登录校验

1.登录校验

首先配置文件中就要加入登录校验部分的配置了,比如jwt的配置,以及配置排除的路由(部分路由是可以不登陆就能访问的,比如查看所有商品的信息)。

hm: jwt: location: classpath:hmall.jks alias: hmall password: hmall123 tokenTTL: 30d auth: excludePaths: - /search/** - /users/login - /items/** - /hi

接下来就是写拦截器,在刚刚的上一节中我们提到了,登录校验是一个全局性的处理,所以我们在网关中统一拦截:

@Component @RequiredArgsConstructor public class AuthGlobalFilter implements GlobalFilter, Ordered { private final AuthProperties authProperties; private final JwtTool jwtTool; 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 (headers != null && !headers.isEmpty()) { token = headers.get(0); } //4.校验并解析token 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(swe); } private boolean isExclude(String path) { for (String excludePath : authProperties.getExcludePaths()) { if(antPathMatcher.match(excludePath,path)){ return true; } } return false; } @Override public int getOrder() { return 0; } }

上述代码中,我们需要两个配置类来读取配置文件中的配置信息:

@Data @Component @ConfigurationProperties(prefix = "hm.auth") public class AuthProperties { private List<String> includePaths; private List<String> excludePaths; }
@Data @ConfigurationProperties(prefix = "hm.jwt") public class JwtProperties { private Resource location; private String password; private String alias; private Duration tokenTTL = Duration.ofMinutes(10); }

这样就能在网关中统一登录校验了

2.网关传递用户信息

(1)网关中存入

这又是另一个问题了,以往在单体架构中,我们可以直接使用TreadLocal拿到用户信息,我们在校验时就将用户信息存入线程上下文中了,当我们进行某个需要确认用户信息的功能时,我们直接拿出来就行了,但是微服务这里就不能这样做了,因为线程模型不同了,网关使用的不再是SpringMVC中阻塞式的传统线程模型,传统式的线程模型非常简单,一个线程就是对应一个响应,而响应式就完全不同了,响应式编程的特点是:

1. 一个请求可能由多个线程处理

2. 一个线程可能处理多个请求的片段

所以我们不再能通过线程来存储和拿取信息了(注意:这里仅指不能在网关,不代表在下游的微服务中不行,网关是响应式的,但下游的微服务不是,所以下游微服务线程唯一,是可以用TreadLocal获取储存信息的)

当然,这也是有办法解决的,既然无法通过线程来存储信息,那我就直接修改请求头,在请求头中存储用户信息。

这里就要用到exchange参数的另一个方法了——mutate():

mutate的意思是突变,这里就是修改请求的意思,这里我们添加一个名为“user-info”的请求头,我们往里面存入用户id,这样下游的微服务就能够通过请求头拿出用户信息(id)了。

//5.传递用户信息 String userInfo = userId.toString(); ServerWebExchange swe = exchange.mutate() .request(builder -> builder.header("user-info", userInfo)) .build(); //放行 return chain.filter(swe);

(2)微服务中解析

存入了用户信息,那我们就要从微服务中拿取,由于很多微服务都需要这个用户信息,所以我们干脆直接做一个拦截器UserInfoInterceptor,在拦截器中解析用户信息然后存入该微服务的TreadLocal。上文提到了,微服务里面是线程唯一的(非响应式的),所以在单个微服务中,都是可以通过TreadLocal来获取上下文的,这里我们使用工具类UserContext存储用户信息:

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.判断是否获取了用户,如果有,存入ThreadLocal if(StrUtil.isNotBlank(userInfo)){ UserContext.setUser(Long.valueOf(userInfo)); } //3.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //清理用户 UserContext.removeUser(); } }
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(); } }

至于拦截器,我们不可能在每个微服务中都重写一次,那样会降低开发效率,所以我们选择直接在所有微服务都要导入的包中写这个拦截器——hm-common包。

注意,这里写完了其实还是不生效的,这个拦截器还需要在MVC中配置:

@Configuration @ConditionalOnClass(DispatcherServlet.class) public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInfoInterceptor()); } }

这里为啥要加@ConditionalOnClass?

首先我们要明确,UserInfoInterceptor 这个拦截器是在微服务中拦截的,不是在网关中拦截,网关只负责将用户信息存入请求头。这个拦截器的目的是解析请求头,所以是在微服务中生效的。但是有一个问题,就是网关也导入了common包,这就会出现另一个问题:

网关是响应式的,不是传统的MVC,所以网关中是找不到SpringMVC的,如果这个拦截器在网关中生效,系统是找不到依赖包的,当然就会报错了。

但是我们又希望这个拦截器在微服务中生效,那很简单啊,只需要排除掉网关不就行了,换句话说就是在有MVC包的模块中生效就行了,所以这里用到了Springboot的自动装配的原理。

于是所有微服务现在都有这个拦截器了,也就能获得用户Id了,比如订单部分我们就可以这样写了:

// 1.5.其它属性 order.setPaymentType(orderFormDTO.getPaymentType()); order.setUserId(UserContext.getUser()); order.setStatus(1);

3.OpenFeign传递用户信息

为什么会又扯到远程调用的问题上了呢?这是因为我们刚刚的拦截器是在网关中存入信息的,如果微服务要获取用户信息,请求必须是标准流程的,即:

浏览器发出—经过网关—网关拦截修改请求头—微服务拦截请求从请求头获取信息

但是别忘了我们还有一种请求方式——远程调用,这个是不经过网关的!!!

这就意味着我们刚刚存入请求头的信息就没用了,因为这是一个新的请求,是由微服务发出、由微服务响应的。

所以我们又要重新修改一次请求头,这次不是在网关里面了,而是在远程调用中修改。

那么又要使用拦截器了,这个拦截器我们写在hm-api中,因为这是管理远程调用的模块,不用远程调用的微服务肯定就不需要被拦截了。

public class DefaultFeignConfig { /** * 配置日志级别 * * @return */ @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } @Bean public RequestInterceptor userInfoRequestInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate requestTemplate) { Long userId = UserContext.getUser(); if (userId != null) { requestTemplate.header("user-info", userId.toString()); } } }; } }

我们当然可以重新写个配置类,这里我和日志级别写在一起了。

所以我们需要在所有要使用远程调用的微服务的启动类上导入这个配置类。

@EnableFeignClients(basePackages = "com.hmall.api.client",defaultConfiguration = DefaultFeignConfig.class) @MapperScan("com.hmall.cart.mapper") @SpringBootApplication public class CartApplication { public static void main(String[] args) { SpringApplication.run(CartApplication.class, args); } @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/20 7:39:00

WinForm DataGridView:单元格类型与高频绘制案例

目录 一、前置准备 二、DataGridView 常用单元格类型&#xff08;基础必掌握&#xff09; 1. 文本框单元格&#xff08;DataGridViewTextBoxColumn&#xff09; 2. 复选框单元格&#xff08;DataGridViewCheckBoxColumn&#xff09; 3. 下拉框单元格&#xff08;DataGridV…

作者头像 李华
网站建设 2025/12/14 19:13:11

java计算机毕业设计社区志愿者服务系统 智慧社区公益志愿协同平台 基层志愿者数字化运营管理系统

计算机毕业设计社区志愿者服务系统38q2o9 &#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。当“志愿红”成为社区里最温暖的底色&#xff0c;传统的人工登记、微信群接龙、纸质工时…

作者头像 李华
网站建设 2025/12/16 20:46:15

考核算法题纠错

考核题算法题纠错 打家劫舍int rob(int* nums, int numsSize) {if (numsSize 0) return 0;if (numsSize 1) return nums[0];int prev_prev nums[0];int prev nums[0] > nums[1] ? nums[0] : nums[1];for (int i 2; i < numsSize; i) {int current prev > (prev…

作者头像 李华
网站建设 2025/12/16 21:11:34

天天劈砖休闲小游戏Linux演示教程

※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※ 本站教程、资源皆在单机环境进行&#xff0c;仅供单机研究学习使用。 ※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※ 一、获取材料和结果演示 百度网盘链接: https://…

作者头像 李华
网站建设 2025/12/30 16:57:53

普中开发板基于51单片机贪吃蛇游戏设计

基于51单片机贪吃蛇游戏设计( proteus仿真程序设计报告讲解视频&#xff09; 仿真图proteus8.17(有低版本) 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;P24 1主要功能&#xff1a; 基于51单片机的贪吃蛇游戏设计 1、采用8*8点…

作者头像 李华
网站建设 2025/12/14 18:43:02

《从零入门 Ascend C:手把手实现高性能向量加法自定义算子》

1. 引言&#xff1a;为什么需要 Ascend C&#xff1f;在深度学习模型训练与推理中&#xff0c;标准算子库&#xff08;如 cuDNN、ACL&#xff09;虽已高度优化&#xff0c;但面对新型网络结构、特殊数据格式或极致性能需求时&#xff0c;往往力不从心。此时&#xff0c;开发者需…

作者头像 李华