本文介绍: 无论是GatewayFilter还是GlobalFilter都支持自定义,只不过编码方式使用方式略有差别。不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory。@Component@Override@Override// 获取请求// 编写过滤器逻辑System.out.println(“过滤器执行了”);// 放行注意:该类的名称一定要以GatewayFilterFactory为后缀!

提示文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


1.网关路由

1.1 认识网关

什么是网关?
顾明思议,网关就是网络的关口数据网络传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全校验
更通俗的来讲,网关就像是以前园区传达室的大爷。

  • 外面的人要想进入园区,必须经过大爷的认可,如果你是不怀好意的人,肯定被直接拦截。
  • 外面的人要传话或送信,要找大爷。大爷帮你带给目标人。

在这里插入图片描述
现在,微服务网关就起到同样的作用。前端请求不能直接访问服务,而是要请求网关:

在SpringCloud当中,提供了两种网关实现方案:

官网
https://spring.io/projects/spring-cloud-gateway#learn
在这里插入图片描述

1.2.快速入门

接下来我们先看下如何利用网关实现请求路由。由于网关本身也是一个独立的微服务,因此也需要创建一个模块开发功能。大概步骤如下:

  1. 创建网关微服务
  2. 引入SpringCloudGateway、NacosDiscovery依赖
  3. 编写启动
  4. 配置网关路由

1.2.1.创建项目
首先,我们要在hmall下创建一个新的module命名hmgateway,作为网关微服务
在这里插入图片描述
1.2.2.引入依赖
在hmgateway模块pom.xml文件中引入依赖

<?xml version="1.0" encoding="UTF-8"?&gt;
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
    <parent>
        <artifactId>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>hm-gateway</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <!--common-->
        <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>
        <!--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>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

1.2.3.启动
在hm-gateway模块的com.hmall.gateway包下新建一个启动类:
在这里插入图片描述
代码如下:

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.2.4.配置路由
接下来,在hm-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/**

1.2.5.测试
启动GatewayApplication,以 http://localhost:8080 拼接微服务接口路径测试。例如:
http://localhost:8080/items/page?pageNo=1&amp;pageSize=1
在这里插入图片描述
此时,启动UserApplication、CartApplication,然后打开前端页面发现相关功能可以正常访问了:

1.3.路由过滤

路由规则的定义语法如下:

spring:
  cloud:
    gateway:
      routes:
        - id: item
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/**

四个属性含义如下:

这里我们重点关注predicates,也就是路由断言。SpringCloudGateway中支持的断言类型有很多:
在这里插入图片描述

2.网关登录校验

单体架构我们需要完成一次用户登录、身份校验,就可以在所有业务获取用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要登录校验,这显然不可取。

2.1.鉴权思路分析

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

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

此时,登录校验的流程如图:
在这里插入图片描述
不过,这里存在几个问题

  • 网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
  • 网关校验JWT之后,如何将用户信息传递给微服务?
  • 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息

2.2.网关过滤器

登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理
在这里插入图片描述
如图所示:

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

那么,该如何实现一个网关过滤器呢?

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

注意:过滤器链之外还有一种过滤器,HttpHeadersFilter,用来处理传递到下游微服务的请求头。例如org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter可以传递代理请求原本的host头到下游微服务。

在这里插入图片描述

Gateway中内置了很多的GatewayFilter,详情可以参考官方文档https://docs.spring.io/spring-cloud-gateway/docs/3.1.7/reference/html/#gatewayfilter-factories
在这里插入图片描述

Gateway内置的GatewayFilter过滤器使用起来非常简单,无需编码,只要在yaml文件简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route.

例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。

使用的使用只需要在application.yaml中这样配置:

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/**

2.3.自定义过滤器

无论是GatewayFilter还是GlobalFilter都支持自定义,只不过编码方式、使用方式略有差别。

2.3.1.自定义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类名称前缀声明过滤器

另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例


@Component
public class PrintAnyGatewayFilterFactory // 父类泛型内部类的Config类型
                extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {

    @Override
    public GatewayFilter apply(Config config) {
        // OrderedGatewayFilter是GatewayFilter的子类,包含两个参数
        // - GatewayFilter:过滤器
        // - int order值:值越小,过滤器执行优先级越高
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                // 获取config
                String a = config.getA();
                String b = config.getB();
                String c = config.getC();
                // 编写过滤器逻辑
                System.out.println("a = " + a);
                System.out.println("b = " + b);
                System.out.println("c = " + c);
                // 放行
                return chain.filter(exchange);
            }
        }, 100);
    }

    // 自定义配置属性成员变量名称很重要,下面会用到
    @Data
    static class Config{
        private String a;
        private String b;
        private String c;
    }
    // 将变量名称依次返回顺序很重要,将来读取参数时需要按顺序获取
    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("a", "b", "c");
    }
        // 返回当前配置类的类型,也就是内部的Config
    @Override
    public Class<Config> getConfigClass() {
        return Config.class;
    }

}

然后在yaml文件中使用:

spring:
  cloud:
    gateway:
      default-filters:
            - PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制

上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数顺序赋值
还有一种用法,无需按照这个顺序,就是手动指定参数名:

spring:
  cloud:
    gateway:
      default-filters:
            - name: PrintAny
              args: # 手动指定参数名,无需按照参数顺序
                a: 1
                b: 2
                c: 3

2.3.2.自定义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;
    }
}

2.4.登录校验

接下来,我们就利用自定义GlobalFilter来完成登录校验。

2.4.1.JWT工具

登录校验需要用到JWT,而且JWT的加密需要秘钥和加密工具。这些在hm-service中已经有了,我们直接拷贝过来:
在这里插入图片描述
具体作用如下:

其中AuthProperties和JwtProperties所需的属性要在application.yaml中配置:

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

2.4.2.登录校验过滤器

接下来,我们定义一个登录校验的过滤器:
在这里插入图片描述

package com.hmall.gateway.filter;

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
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();
        }

        // TODO 5.如果有效,传递用户信息
        System.out.println("userId = " + userId);
        // 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;
    }
}

在这里插入图片描述
在这里插入图片描述

2.5.微服务获取用户

现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?

由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。

据图流程图如下:
在这里插入图片描述
因此,接下来我们要做的事情有:

  • 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
  • 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行

2.5.1.保存用户到请求头

首先,我们修改登录校验拦截器处理逻辑,保存用户信息到请求头中:
在这里插入图片描述

2.5.2.拦截器获取用户

在hm-common中已经有一个用于保存登录用户的ThreadLocal工具:
在这里插入图片描述
其中已经提供了保存和获取用户的方法
在这里插入图片描述
接下来,我们只需要编写拦截器,获取用户信息并保存到UserContext然后放行即可

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

我们在hm-common模块下定义一个拦截器
在这里插入图片描述
具体代码如下:

package com.hmall.common.interceptor;

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();
    }
}

接着在hm-common模块下编写SpringMVC的配置类,配置登录拦截器:
在这里插入图片描述

package com.hmall.common.config;

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());
    }
}

2.6.OpenFeign传递用户

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

但有些业务比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务流程如下:
在这里插入图片描述
在这里插入图片描述

public interface RequestInterceptor {

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

在这里插入图片描述
在com.hmall.api.config.DefaultFeignConfig中添加一个Bean:

@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调用时也会传递登录用户信息了。

原文地址:https://blog.csdn.net/weixin_48052161/article/details/134664611

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。

如若转载,请注明出处:http://www.7code.cn/show_5577.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注