Spring Cloud Gateway的使用

Spring Cloud Gateway

网关:微服务中最边缘的服务,用来做用户和微服务的桥梁

  • 没有网关❓:客户端直接访问我们的微服务,会需要在客户端配置很多的ip和端口,如果服务器并发比较大则无法完成负载均衡
  • 有网关❓:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的ip:port)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可以实现token拦截,权限验证,等等操作…

网关组件:gatwayzuul

  • gatway是springcloud官方提供的,用于取代zuul
    在这里插入图片描述

核心逻辑:路由转发+执行过滤器链

三大核心概念

Route(路由)

eureka结合做动态路由

组成:

  • 由一个ID、一个URL、一组断言工厂、一组Filter组成
  • 如果路由断言成真,说明请求URL和配置路由匹配

Predicate(断言)

其实就是一个返回true,false的表达式

用于匹配信息做路由限制的

Filter(过滤)

Spring Cloud Gateway中的Filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理

  • 一个是针对某一个路由的filter(例如对单个接口做限制)
  • 一个是针对全局的filter(例如全局效验token)

开始使用

创建两个模块,一个login-service,一个gateway-server

  • login-service:暂时使用springweb依赖即可
  • gateway-server:暂时使用gateway依赖即可

pom.xml中依赖(例如jar)的版本与前期教程版本号一致

先编写一个接口在login-service

LoginController.java:

@RestController
public class LoginController {
    
    
    @GetMapping("doLogin")
    public String doLogin(String name,String pwd){
    
    
        System.out.println(name+"密码:"+pwd);
        String token= UUID.randomUUID().toString();
        return token;
    }
}

动态路由

  • 配置方式的路由
  • 代码方式的路由

两者不会冲突

准备依赖

login-service:

多一个eureka-client依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pengke</groupId>
    <artifactId>login-service02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>login-service02</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

gateway-server:

也是多一个eureka-client依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

启动类统一添加启动客户端服务注解

@EnableEurekaClient

配置路由

在login-service模块中进行配置文件配置

server:
  port: 8081
spring:
  application:
    name: login-service
eureka:
  client:
    service-url:
      defaultZone: eureka远程服务端地址
    registry-fetch-interval-seconds: 3 # 网关拉取服务列表的时间缩短
  instance:
    hostname: localhost
    instance-id: ${
    
    eureka.instance.hostname}:${
    
    spring.application.name}:${
    
    server.port}

在gateway-server模块中进行配置文件配置

server:
  port: 80
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: true # 开启动态路由
          lower-case-service-id: true # 开启服务名称小写
      routes:
          -   id: login-service-route # 这个是路由id,保持唯一即可
              uri: http://localhost:8081 # uri:统一资源定位符  url:统一资源标识符
#             uri: lb://login-service # 使用lb协议微服务名称做负载均衡
              predicates: # 断言是给某个路由设定的
                - Path=/doLogin # 匹配规则 只要Path匹配上/doLogin就往uri转发,并且将路径带上
eureka:
  client:
    service-url:
      defaultZone: eureka远程服务端地址
    registry-fetch-interval-seconds: 3 # 网关拉取服务列表的时间缩短
  instance:
    hostname: localhost
    instance-id: ${
    
    eureka.instance.hostname}:${
    
    spring.application.name}:${
    
    server.port}

启动两个服务,在浏览器中输入localhost/doLogin或者localhost:8081/doLogin或者localhost/login-service/doLogin都可实现访问

代码方式的路由:

gateway-server模块中创建config---》RouteConfig

  • .route(“路由id”,函数式访问路径.uri(请求根路径)).build()
@Configuration
public class RouteConfig {
    
    
    /**
     * 代码路由
     * 和配置形式路由不冲突
     * @param builder
     * @return
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    
    
        return builder.routes()
                .route("test-id",r->r.path("/m_xiaozhilei").uri("https://blog.csdn.net"))
                .build();
    }
}

启动服务浏览器访问http://localhost/gateway-server/m_xiaozhilei或者http://localhost/m_xiaozhilei都可访问到最终请求的页面

这样就用会了动态路由!

断言

在配置文件中配置predicates属性

  • 断言是给某个路由设定的

在这里插入图片描述

主要有以上这些限制

例如:

 predicates:
	- After=2023-03-22T09:42:49.521+08:00[Asia/Shanghai]

即可实现请求时间在2023-03-22…之后请求才可以访问成功…

过滤器

按生命周期分两种

  • pre:在业务逻辑前
  • post:在业务逻辑后

按种类分也是两种

  • GatewayFilter:需要配置某个路由才能过滤,如果需要使用全局路由,需要配置Defilters
  • GlobalFilter:全局过滤器,不需要配置路由,系统初始化作用到所有路由上

在网关gatewayserver模块创建过滤器

filter=》MyGlobalFilter.java

/**
 * 定义过滤器
 */
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    
    

    /**
     * 过滤方法
     * 过滤器链模式
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        //针对请求的过滤
        ServerHttpRequest req=exchange.getRequest();
        String path=req.getURI().getPath();
        HttpHeaders header=req.getHeaders();
        String methods=req.getMethod().name();
        String host=req.getRemoteAddress().getHostName();
        String ip=req.getHeaders().getHost().getHostString();
        System.out.println(path+"header="+header+"方法="+methods+"host="+host+"ip="+ip);

        //响应的数据
        ServerHttpResponse resp=exchange.getResponse();
        resp.getHeaders().set("content-type","application/json;charset=utf-8");
        Map map=new HashMap();
        map.put("code",500);
        map.put("msg","token有误");
        ObjectMapper objectMapper=new ObjectMapper();
        //将map转字节
        byte[] bytes=new byte[0];
        try {
    
    
            bytes=objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
    
    
            e.printStackTrace();
        }
        //通过buffer工厂将字节数组包装成一个数据包
        DataBuffer warp=resp.bufferFactory().wrap(bytes);
        if(1>0){
    
    //判断一下(例如判断token是否正确)
            return resp.writeWith(Mono.just(warp));
        }else{
    
    
            //放行到下一个过滤器
            return chain.filter(exchange);
        }
    }

    /**
     * 指定顺序的方法
     * 越小越先执行
     * @return
     */
    @Override
    public int getOrder() {
    
    
        return 0;
    }
}

实现GlobalFilter和Ordered的方法

  • 过滤器中chain.filter(exchange)表示放行
  • 过滤器获取相关参数在上方代码已示例
  • ordered中getOrder方法用来指定过滤器的执行顺序

通过上方即可实现过滤器,再次访问页面时则会返回{msg:token有误,coed:500}

实现Token+IP验证拦截

首先简单实现一下IP拦截,常用于黑名单,白名单

  • 在网关中定义一个IP拦截过滤器
@Component
public class IPcheckFilter implements GlobalFilter, Ordered {
    
    
    /**
     * 网关的并发比较高  不要在网关里直接操作数据库
     */
    public static final List<String> BLACK_LIST= Arrays.asList("127.0.0.1","144.125.231.14");
    /**
     * 拿到ip进行校验决定是否拦截
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request=exchange.getRequest();
        String ip=request.getHeaders().getHost().getHostString();
        //这里查数据库获取黑名单或者白名单进行相关操作

        if(!BLACK_LIST.contains(ip)){
    
    
            //不是黑名单放行
            return chain.filter(exchange);
        }
        //拦截
        ServerHttpResponse response=exchange.getResponse();
        response.getHeaders().set("context-type","application/json;charset=utf-8");
        HashMap<String,Object> map=new HashMap<>(4);
        map.put("code",438);
        map.put("msg","黑名单禁止访问");
        ObjectMapper objectMapper=new ObjectMapper();
        byte[] bytes= new byte[0];
        try {
    
    
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
    
    
            e.printStackTrace();
        }
        DataBuffer wrap=response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
    
    
        return -5;
    }
}

代码如上,思路解析:

  • 定义一个常量用于存储黑名单ip或者白名单ip,这里我存的是黑名单ip(业务中常常存与临时存储处,这里演示我就定个变量去存储)
  • 通过.contains进行判断是否存在
  • 通过chain.filter(exchange)进行放行
  • 如果验证失败则可返相关信息告知访问者

开始实现token拦截验证

这里使用redis进行存储token(不会redis可查阅我另俩篇文章redis的开始使用--------->redis与springboot整合)

引入redis依赖

<!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

在上方doLogin接口中存储登陆者的信息以及为它生成的token,这里我创了个User实体类用于模拟真实登陆场景

 @Autowired
    public StringRedisTemplate redisTemplate;
    @GetMapping("doLogin")
    public String doLogin(String name,String pwd){
    
    
        System.out.println(name+"密码:"+pwd);
        User user=new User(1,name,pwd,20);
        //token
        String token= UUID.randomUUID().toString();
        //存在radis
        redisTemplate.opsForValue().set(token,user.toString(), Duration.ofSeconds(7200));
        return token;
    }

在网关中编写token拦截接口

//指定放行路径
    public static final List<String> ALLOW_URL= Arrays.asList("/login-service/doLogin","/myUrl");
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 约定好请求头携带 Authorization value:bearer token
     * - 拿到url效验决定是否需要token效验
     * - 拿到请求头
     * - 拿到token效验
     * - 决定是否放行
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request=exchange.getRequest();
        String path=request.getURI().getPath();
        if(ALLOW_URL.contains(path)){
    
    
            return chain.filter(exchange);//放行不进行token效验
        }
        HttpHeaders headers=request.getHeaders();
        List<String> authorization=headers.get("Authorization");
        if(!CollectionUtils.isEmpty(authorization)){
    
    
            //携带authorization了
            String token=authorization.get(0);
            if(StringUtils.hasText(token)){
    
    
                //约定好的前缀 bearer token
                String realToken=token.replaceFirst("bearer ","");
                if(StringUtils.hasText(realToken)&&redisTemplate.hasKey(realToken)){
    
    
                    //真携带了token
                    return chain.filter(exchange);
                }
            }

        }
        //拦截
        ServerHttpResponse response=exchange.getResponse();
        response.getHeaders().set("context-type","application/json;charset=utf-8");
        HashMap<String,Object> map=new HashMap<>(4);
        map.put("code",403);
        map.put("msg","无权限访问");
        ObjectMapper objectMapper=new ObjectMapper();
        byte[] bytes= new byte[0];
        try {
    
    
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
    
    
            e.printStackTrace();
        }
        DataBuffer wrap=response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }

首先指定放行非需要token的请求,例如登陆接口!!!

大概验证流程为代码中的注释

通过redisTemplate.hasKey(token)去匹对是否有该token来决定是否允许访问该接口

最终就实现了一个基本的业务拦截需求了~✌

下一篇讲解实现redis限流!

猜你喜欢

转载自blog.csdn.net/m_xiaozhilei/article/details/129205051