09、SpringCloud之Gateway网关组件学习笔记(下)

举报
长路 发表于 2022/11/28 20:10:48 2022/11/28
【摘要】 Gitee仓库、Github仓库博客目录索引(持续更新)动力节点最新SpringCloud视频教程|最适合自学的springcloud+springcloudAlibabaPS本章节中部分图片是直接引用学习课程课件,如有侵权,请联系删除。SpringCloudGateway是SpringCloud的一个全新的API网关项目,目的是为了替换掉Zuul1。技术选型性能方面性能⾼于Zuul,官⽅测试,S

6.3:实战4:实现一个ip拦截的过滤器

思路:同样也是在Gateway网关中添加一个全局过滤器组件。

image-20220730085017659

package com.changlu.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Description: IP检查过滤器
 * @Author: changlu
 * @Date: 8:41 AM
 */
@Component
public class IPCheckFilter implements GlobalFilter, Ordered {

    /**
     * 网关的并发比较高 不要再网关里面直接操作mysql
     * 后台系统可以查询数据库 用户量 并发量不大
     * 如果并发量大 可以查redis 或者 在内存中写好
     */
    private static final List<String> BLACK_LIST = Arrays.asList("127.0.0.1", "192.168.1.1");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取到请求对象化
        ServerHttpRequest request = exchange.getRequest();
        String ip = request.getHeaders().getHost().getHostString();
        //若是在集合中出现该ip,那么此时就拦截响应(一般黑名单可以存储在数据库中也可以存储的redis里)
        if (!BLACK_LIST.contains(ip)) {
            chain.filter(exchange);
        }
        //若是存在就进行拦截,并响应
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("content-type", "application/json;charset=utf-8");
        Map<String, Object> result = new HashMap<>();
        result.put("code", 438);
        result.put("msg", "你已被拉黑,无法访问");
        ObjectMapper objectMapper = new ObjectMapper();
        byte[] data = new byte[0];
        try {
            data = objectMapper.writeValueAsBytes(result);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(data);
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

测试一下

可以看到localhost是在拦截范围内的,所以gateway会进行拦截响应:

image-20220730085559229

image-20220730085640102


6.4、实战5:在网关中实现token认证校验

在实战5中,我们完成的就是下图的第7步骤,也就是token进行认证校验是否合法来进行放行或直接响应!

image-20220730090640844

说明:本章节的话会在login-service中完善doLogin接口,接着在gateway服务里添加一个认证token过滤器,并新建一个user-service并在其中添加一个接口对外使用。

注意:本章节的重点是在gateway中实现token认证来达到放行or错误响应,并不是在登录接口存储用户信息这些细节上,对于token生成、校验以及用户认证都仅仅只是做了简单的实现。

login-service(增加登录接口)

image-20220730100222057

domain/user.java

package com.changlu.loginservice.domain;

import lombok.Data;

import java.io.Serializable;

/**
 * @Description: 用户实体类
 * @Author: changlu
 * @Date: 9:20 AM
 */
@Data
public class User implements Serializable {
    private String username;
    private String password;
}

1、硬编码指定一个token。

private static final String token = "700d7a8d-262a-447a-8254-9dd9ead6a0e2";

2、添加一个doLogin接口,来用于获取token。

@PostMapping("/doLogin")
public String doLogin(@RequestBody User user) {
    System.out.println("dologin进行登录:" + user);
    //数据库进行认证,这里的话直接返回一个token
    return token;
}

user-service模块(新增,添加一个对外界接口)

说明:该模块主要是用于测试之后携带token的接口是否能够通过gateway认证并进行转发。

image-20220730100601631

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>

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

配置文件:application.yaml

server:
  port: 8082
spring:
  application:
    name: user-service

# 注册目标
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    hostname: localhost
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

提供一个用户接口:仅仅是进行简单的用户返回。

package com.changlu.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description:
 * @Author: changlu
 * @Date: 9:16 AM
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping
    public Map<String, Object> getUser() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "成功获取到用户信息");
        return result;
    }

}

gateway-server模块(添加token认证过滤器)

image-20220730100834260

1、添加一个token过滤器

package com.changlu.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * @Description: token检查过滤器
 * @Author: changlu
 * @Date: 9:25 AM
 */
@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {

    private static final String token = "700d7a8d-262a-447a-8254-9dd9ead6a0e2";

    private static final List<String> WHITE_PATH = Arrays.asList("/doLogin");

    /**
     * 流程:1、路径检测(是否放行)。2、请求头token获取。3、校验:放行or直接响应
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //放行一些公开接口
        String path = request.getURI().getPath();
        if (WHITE_PATH.contains(path)) {
            return chain.filter(exchange);
        }
        //从请求头中获取到Authorization
        List<String> authorization = request.getHeaders().get("Authorization");
        if (!ObjectUtils.isEmpty(authorization)) {
            String token = authorization.get(0);
            //去掉前缀"bearer "
            token = token.replaceFirst("Bearer ", "");
            //token校验,成功放行(实际上会进行token解析取到uuid来从redis中获取,这里简单来表示一下)
            if (TokenCheckFilter.token.equals(token)) {
                return chain.filter(exchange);
            }
        }
        //失败进行错误响应
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("content-type", "application/json;charset=utf-8");
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", HttpStatus.UNAUTHORIZED.value());
        result.put("msg", "暂未授权");
        ObjectMapper objectMapper = new ObjectMapper();//jackson工具类
        byte[] data = new byte[0];
        try {
            data = objectMapper.writeValueAsBytes(result);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(data);
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
        return 2;
    }
}

2、编写配置文件,新增一个路由

image-20220730100910677

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      enabled: true   # 默认开启,只要加了网关依赖
      routes:
          # 用户服务路由
        - id: user-service-route
          uri: lb://user-service
          predicates:
            - Path=/user

测试

image-20220730101119026

我们启动这四个模块,分别是:注册中心、网关、登录服务、用户服务。

来启动服务,以及查看一下eureka的注册中心服务注册情况:

image-20220730101226721

image-20220730101305041

接下来就可以开始进行测试了:我准备好两个接口

image-20220730101339630

①测试doLogin接口是否能够放行并返回token

image-20220730101359965

②测试用户服务接口

首先添加一下token,接着来发送请求

image-20220730101438001

image-20220730101510327

那我们来故意写错token来发送一下:

image-20220730101548304

image-20220730101557684


七、实战系列

7.1、实战6:实现请求限流

7.1.1、认识限流

通俗的说,限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,限流大致分为两种:

  1. IP 限流(5s 内同一个 ip 访问超过 3 次,则限制不让访问,过一段时间才可继续访问)
  2. 请求量限流(只要在一段时间内(窗口期),请求次数达到阀值,就直接拒绝后面来的访问了,过一段时间才可以继续访问)(粒度可以细化到一个 api(url),一个服务)

7.1.2、限流模型

介绍限流模型

限流模型:漏斗算法,令牌桶算法,窗口滑动算法,计数器算法。

常用的模型分类有两种:

  • 时间模型
    • 固定窗口模型:timeline 按照固定间隔分窗口,每个窗口有一个独立计数器,每个计数器统计窗口内的 qps,如果达到阈值则拒绝服务。最简单的限流模型,但是缺点比较明显,当在临界点出现大流量冲击,就无法满足流量控制。
    • 滑动窗口模型:滑动时间模型会将每个窗口切分成 N 个子窗口,每个子窗口独立计数。这样用w1+w2计数之和来做限流阈值校验,就可以解决此问题。
  • 桶模型
    • 令牌桶:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
      • 解决了在实际上的互联网应用中,流量经常是突发性的问题。
    • 漏桶:水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

本章实战说明

本章节的话就使用Gateway内置的一个限流过滤器RequestRateLimiterGatewayFilterFactory

也就是令牌桶限流模型入不敷出

1)、所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;

2)、根据限流大小,设置按照一定的速率往桶里添加令牌;

3)、桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;

4)、请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完

业务逻辑之后,将令牌直接删除;

5)、令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令

牌,以此保证足够的限流;

image-20220730103021565


7.1.3、Gateway 结合 redis 实现请求量限流(Gateway内置限流令牌桶实现)

集成过程

注意:Spring Cloud Gateway 已经内置了一个 RequestRateLimiterGatewayFilterFactory,该过滤器是针对于某个路由的,并不是全局过滤器。

image-20220730115221049

1、添加redis依赖

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

2、指定限流的内容:ip或接口

config/RequestLimitConfig.java

package com.changlu.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;


/**
 * @Description: 请求限流配置类
 * @Author: changlu
 * @Date: 10:34 AM
 */
@Configuration
public class RequestLimitConfig {

    //针对某一个ip地址来进行限流(例如:localhost)
    @Bean(name = "ipKeyResolver")
    @Primary
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getHeaders().getHost().getHostString());
    }

    //针对某一个接口uri来进行限流(例如:/doLogin)
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }
}

3、配置文件为指定的路由配置filter:

配置文件:application.yaml

image-20220730115443165

# redis参数配置
redis:
    host: localhost
    port: 6379
    database: 0
    password: 123456
# 配置路由
filters:
  - name: RequestRateLimiter
    args:
        key-resolver: '#{@ipKeyResolver}'
        redis-rate-limiter.replenishRate: 1 #令牌每秒填充速度
        redis-rate-limiter.burstCapacity: 1 #桶大小
        redis-rate-limiter.requestedTokens: 1 #默认是1,每次请求消耗的令牌数

测试

image-20220730115653092

使用jmeter来进行测试:

image-20220730115636649

若是请求失败,默认就会返回响应码为429。

image-20220730115726137

看一下redis中存储的参数:

image-20220730115758095

我们也可以换之前配置指定的另一个参数也就是接口名,此时redis中存储的如下:

image-20220730120044283

image-20220730120029882

7.2、实战7:Gateway集成跨域配置

对于ajax 同源策略,例如前端的访问端口与后端访问的端口不一致时,也就会产生跨域问题。

方式一:参数配置

spring:
  cloud:
    gateway:
        globalcors:
          cors-configurations:
            '[/**]':
              allowedOrigins: "*"
              allowedMethods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION

方式二:通过java配置过滤器

@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
 
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
 
        return new CorsWebFilter(source);
    }
}

测试

准备一个ajax的跨域问题:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="button" value="触发按钮" onclick="getData()">
<script src="http://apps.bdimg.com/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
    function getData() {
        //ajax请求
        $.get('http://localhost:81/doLogin',function(data){
            alert(data);
        });
    }
</script>
</body>
</html>

image-20220730132931661

配置完跨域后再来进行测试:

image-20220730133253964

参考文章

[1]. 如何设置nginx日志格式来查看负载分担结果

[2]. Nginx负载均衡配置+记录请求分发日志

[3]. Spring Cloud Gateway-自定义断言及过滤器

[4]. spring-cloud-gateway 11 限流 RequestRateLimiterGatewayFilterFactory

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。