GateWay网关


title: GateWay网关
tag: 笔记 微服务组件


image-20240324154836470

GateWay网关

在以前的单体项目中,前端的请求不管什么模块的操作都只需要发送到这个单体项目就可以完成处理。而现在使用微服务将单体项目拆分为多个服务之后这样的方式就行不通了。

我们需要一个类似之前学习的SpringMVC的DispatcherServlet将请求分发到不同的Handler一样,我们需要一个微服务入口来将请求转发到其对应的服务实例进行处理

网关

网关就是络的口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验

前端请求不能直接访问微服务,而是要请求网关:

  • 网关可以做安全控制,也就是登录身份校验,校验通过才放行
  • 通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
image-20240324154836470

网关路由

这里我们简单实现网关的基本路由功能。

  1. 创建模块导入依赖

由于网关也是一个服务,我们需要再创建一个GateWay模块作为网关服务,GateWay需要发现微服务的其它实例,因此也需要注册到Nacos中。

<!--网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--nacos discovery-->
        <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>
  • 分别导入gateway、nacos-discovery、loadbalancer依赖。
  1. 然后编写网关的启动类:
package com.hmall.gateway;

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);
    }
}
  1. 配置路由

gateway模块的resources目录新建一个application.yaml文件,内容如下:

server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848
    gateway:
      routes:
        - id: item # 路由规则id,自定义,唯一
          uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
          predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
            - Path=/items/**,/search/** # 这里是以请求路径作为判断规则
        - id: cart
          uri: lb://cart-service
          predicates:
            - Path=/carts/**
        - id: user
          uri: lb://user-service
          predicates:
            - Path=/users/**,/addresses/**
        - id: trade
          uri: lb://trade-service
          predicates:
            - Path=/orders/**
        - id: pay
          uri: lb://pay-service
          predicates:
            - Path=/pay-orders/**

四个属性含义如下:

  • id:路由的唯一标示
  • predicates:路由断言,其实就是匹配条件
  • filters:路由过滤条件,后面讲
  • uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。
  1. 测试

启动GatewayApplication,以 http://localhost:8080 拼接微服务接口路径来测试。例如:

http://localhost:8080/items/page?pageNo=1&pageSize=1

发现成功返回数据,网关配置成功。

网关校验

网关除了路由转发之外还有一个重要的功能就是对请求进行一些必要的校验和处理

比如我们项目中常用的登录状态校验。在之前的单体项目中,我们的登录状态校验通常使用SpringMVC提供的拦截器来完成,而现在一个单体项目被拆分成了多个SpringBoot项目,在每个服务实例中都编写拦截器做服务校验显然是不合理的。而我们的网关是微服务的入口,我们完全可以在网关中对登录进行校验。

这里我们需要使用GateWay提供的GateWayFilter拦截器来处理请求,我们这里就用常见的登录校验来作为例子来实现一个微服务中的登录校验。

登录校验思路

我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:

  • 每个微服务都需要知道JWT的秘钥,不安全
  • 每个微服务重复编写登录校验代码、权限校验代码,麻烦

既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:

  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能

此时的登录流程:

image-20240324164229564

不过,这里存在几个问题:

  • 网关校验JWT之后,如何将用户信息传递给微服务?
  • 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?

实际网关的路由和微服务之间的调用都是HTTP请求,我们可以在请求头上加入用户信息即可,这在后面会解决。

网关过滤器

我们之前提到过我们可以使用GateWay提供的GateWayFilter拦截器来处理请求,那么GateWayFilter是如何使用的呢?

我们先看GateWay的生效基本流程:

image-20240324165139697
  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为**Filter**)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

GateWay内置了一些自定义的过滤器,但在这里我们先只关注如何自定义过滤器。

自定义GatewayFilter

自定义GatewayFilter不是直接实现GatewayFilter,而是实现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);
            }
        };
    }
}

注意:该类的名称一定要以GatewayFilterFactory为后缀!

然后在yaml配置中这样使用:

spring:
  cloud:
    gateway:
      default-filters:
            - PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器

GatewayFilter还可以指定参数,比较复杂不做展示,我们重点来看GlobalFilter,我们使用它来完成登录登录校验。

自定义GlobalFilter

自定义GlobalFilter则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数:

@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    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

我们这里的登录需要使用到JWT,因此我们需要引入一些JWT需要的资源和配置:

image-20240326182244524
  • AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
  • JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置
  • SecurityConfig:工具的自动装配
  • JwtTool:JWT工具,其中包含了校验和解析token的功能
  • hmall.jks:秘钥文件

其中AuthPropertiesJwtProperties所需的属性要在application.yaml中配置:

hm:
  jwt:
    location: classpath:hmall.jks # 秘钥地址
    alias: hmall # 秘钥别名
    password: hmall123 # 秘钥文件密码
    tokenTTL: 30m # 登录有效期
  auth:
    excludePaths: # 无需登录校验的路径
      - /search/**
      - /users/login
      - /items/**

登录校验过滤器

接下来我们实现一个登录校验过滤器:

@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) {
        ServerHttpRequest request = exchange.getRequest();
        if(isExclude(request.getPath().toString())){
            // 无需拦截,直接放行
            return chain.filter(exchange);
        }
        List<String> authorization = request.getHeaders().get("authorization");
        String token = null;
        if(!CollUtils.isEmpty(authorization)) token = authorization.get(0);
        Long userId;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 如果无效,拦截,返回401错误
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(401);
            return response.setComplete();
        }
        ServerWebExchange swe = exchange.mutate().request(r
                -> r.header("user-info", userId.toString())).build();
        return chain.filter(swe);
    }
    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;
    }
}

代码大致逻辑:

  1. 拿到request,若url不符合拦截路径则直接放行。
  2. 通过request从请求头中拿到authorization
  3. authorization不存在或者不合法则返回401错误。
  4. 将从authorization中解析出来的用户信息放入request中。

微服务获取用户

现在经过网关的处理之后,我们的请求头中应该包含了一个user-info的请求头包含着我们的当前登录用户的id。

我们现在可以像之前的单体项目一样使用一个拦截器去拿到用户id并存储到ThreadLocal中:

image-20240326184248253

由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在hm-common中,并写好自动装配。这样微服务只需要引入hm-common就可以直接具备拦截器功能,无需重复编写。

我们在common模块编写一个拦截器:

public class UserInfoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String userID = request.getHeader("user-info");
        if(StrUtil.isNotBlank(userID)){
            UserContext.setUser(Long.valueOf(userID));
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.removeUser();
    }
}

然后我们需要在MVC的配置中添加这个拦截器:

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

但在这里我们还存在两个问题:

  1. Common模块与其它微服务模块不在同一个模块下,微服务模块如何加载到MvcConfig

SpringBoot提供了Spring.factories的机制装配Bean,因此我们在resouce下的创建Spring.factories文件并配置自动装配的Bean即可:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.JsonConfig,\
  com.hmall.common.config.MvcConfig
  1. GateWay模块不基于SpringMVC,但导入了Common模块,我们如何使MvcConfig在GateWay中不生效?

Spring提供了@ConditionalOnxxx注解来指定条件判断该类是否加载,我们知道GateWay模块不基于SpringMVC,而SpringMVC存在一个最基本的类DispatcherServlet,因此我们可以使用DispatcherServlet来指定该配置类是否生效

我们在MvcConfig 中加上注解@ConditionalOnClass(DispatcherServlet.class)

OpenFeign传递用户

前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。

但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:

image-20240326185742726

因此我们还需要考虑在微服务远程调用时如何带上用户的信息

因为之前我们在各个服务中添加了拦截器在请求头,因此我们可以在OpenFeign调用时在request请求头上再添加user-info请求头存放用户信息即可。

这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor

这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor

public interface RequestInterceptor {

  /**
   * Called for every request. 
   * Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}

我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。

因此我们可以在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());
        }
    };
}

questInterceptor 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`请求时都会在请求头中添加`user-info`属性。
  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值