SpringCloudAlibaba:服务网关之Gateway学习

news2024/12/23 17:31:01

目录

一、网关简介

(一)为什么要用网关

(二)网关解决了什么问题

(三)常用的网关

二、Gateway简介

(一)核心概念

(二)工作原理

三、Gateway快速入门

(一) 基础版

(二) 增强版

(三) 简写版

四、断言

问题总结:

内置路由断言工厂

五、过滤器

(一)网关过滤器(GatewayFilter)

(二)全局过滤器(GlobalFilter)

1、自定义全局过滤器-token验证

2、自定义全局过滤器-鉴权

六、网关限流

(一)接口限流

使用Postman测试Spring Cloud Gateway的限流器有以下步骤:

(二)Gateway整合Sentinel实现网关限流

1.网关如何限流?

2.实战演示


一、网关简介

(一)为什么要用网关

大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用 这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。

左图这样的架构,会存在着诸多的问题:

  • 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性

  • 认证复杂,每个服务都需要独立认证。

  • 存在跨域请求,在一定场景下处理相对复杂。

(二)网关解决了什么问题

 

(三)常用的网关

在业界比较流行的网关,有下面这些:

  • Ngnix+lua: 使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用 lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本。

  • Kong:基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。 问题:只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。

  • Zuul Netflix开源的网关,功能丰富,使用JAVA开发,易于二次开发 问题:缺乏管控,无法动态配置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx

  • Spring Cloud Gateway :Spring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。

注意:SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway 来做网关。

二、Gateway简介

Gateway官网

(一)核心概念

 id,路由标识符,区别于其他 Route。

uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。

order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。

predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。

filter,过滤器用于修改请求和响应信息。

(二)工作原理

执行流程大体如下:

  • Gateway Client向Gateway Server发送请求

  • 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文

  • 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping

  • RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用

  • 如果过断言成功,由FilteringWebHandler创建过滤器链并调用

  • 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应

三、Gateway快速入门

(一) 基础版

第1步:创建一个 api-gateway 的模块,导入相关依赖

    <dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
​

第2步: 创建主类

@SpringBootApplication
public class GateApplication {
    public static void main(String[] args) {
        SpringApplication.run(GateApplication.class,args);
    }
}

第3步: 添加配置文件

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:  # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
        - id: user_server  # 当前路由的标识, 要求唯一
          uri: http://127.0.0.1:8006  # 请求要转发到的地址
          order: 1  # 路由的优先级,数字越小级别越高
          predicates:  # 断言(就是路由转发要满足的条件)
            - Path=/user-server/**  # 当请求路径满足Path指定的规则时,才进行路由转发
          filters:  # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1  # 转发之前去掉1层路径

第4步: 启动项目, 并通过网关去访问微服务

http://127.0.0.1:7000/user-server/user/get1    http://127.0.0.1:8006/user/get1

以上两个请求的效果是相同的

(二) 增强版

现在在配置文件中写死了转发路径的地址, 前面我们已经分析过地址写死带来的问题, 接下来我们从注册中心获取此地址。

第1步:加入nacos依赖

<!--nacos客户端依赖--> 基础版的以来中已经有了

第2步:在主类上添加注解

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

第3步:修改配置文件

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务
      routes:
        - id: user_server
          uri: lb://user-server   # lb指的是从nacos中按照名称获取微服务,并遵循负
          order: 1
          predicates:
            - Path=/user-server/**
          filters:
            - StripPrefix=1
server:
  port: 7000

第4步:测试 http://127.0.0.1:7000/user-server/user/get1

(三) 简写版

第1步: 去掉关于路由的配置

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务
server:
  port: 7000

第2步: 启动项目,并通过网关去访问微服务 http://127.0.0.1:7000/user-server/user/get1 这时候,就发现只要按照网关地址/微服务/接口的格式去访问,就可以得到成功响应

四、断言

Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。 断言就是说: 在 什么条件下 才能进行路由转发

问题总结:

1.在Spring Cloud Gateway中,如果我们启用了service discovery(配置了gateway.discovery.locator.enabled=true),那么对于没有配置路由映射的微服务,其请求是否可以正常访问?

答案是:可以正常访问。启用service discovery后,Spring Cloud Gateway会自动根据服务名创建路由配置。

2.在Spring Cloud Gateway的路由规则中,Path断言中的路径值user-server是否可以随意填写?

答案是:不能随意填写,它必须与实际的服务地址或路由相对应。以路由规则为例:

      routes:
        - id: user_server
          uri: lb://user-server  
          order: 1
          predicates:  
            - Path=/user-server/**

这里的Path=/user-server/** 表示,任何以/user-server/开头的请求路径都会被该路由规则匹配并转发。但这里的user-server必须与uri的值 lb://xn--user-server-9n3uz6x 。lb表示从服务注册中心获取实际服务地址。所以,如果Path的值写成/other-server/**,则该路由规则的Path断言将永远不会匹配任何请求,造成死路由。

正确的访问路径应该是:/user-server/some/path如果写成:/other-server/some/path则该请求无法找到任何匹配的路由进行转发,会执行fallback配置的逻辑。

所以,Path断言中的路径不可以随意填写,它必须与实际的后端服务地址或路由URI对应,否则会产生如下问题: 死路由、路径混淆、无法访问。


内置路由断言工厂

SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体如下:

  1. 基于Datetime类型的断言工厂

    此类型的断言根据时间做判断,主要有三个: AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期 BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期 BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内

    spring:
      application:
        name: gateway
      cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848
        gateway:
          discovery:
            locator:
              enabled: true
          routes:
          - id: after_route
            uri: https://example.org
            predicates:
                - After=2017-01-20T17:42:47.789-07:00[America/Denver] # 时间点后匹配
                - Before=2017-01-20T17:42:47.789-07:00[America/Denver] # 时间点前匹配
                - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] # 时间区间匹配
          - id:    
  2. 基于远程地址的断言工厂

    RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中

            predicates:
                - RemoteAddr=192.168.1.1/24
  3. 基于Cookie的断言工厂

    CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求 cookie是否具有给定名称且值与正则表达式匹配。

            predicates:
                - Cookie=chocolate, ch.p
  4. 基于Header的断言工厂 HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。判断请求Header是否 具有给定名称且值与正则表达式匹配。

            predicates:
                - Header=X-Request-Id, \d+
  5. 基于Host的断言工厂 HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。

            predicates:
                - Host=**.somehost.org,**.anotherhost.org
  6. 基于Method请求方法的断言工厂 MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。

            predicates:
                - Method=GET,POST
  7. 基于Path请求路径的断言工厂 PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。

            predicates:
                - Path=/red/{segment},/blue/{segment}
  8. 基于Query请求参数的断言工厂 QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具 有给定名称且值与正则表达式匹配。

            predicates:
                - Query=green
  9. 基于路由权重的断言工厂 WeightRoutePredicateFactory:接收一个[组名,权重],然后对于同一个组内的路由按照权重转发

    该路由会将约90%的流量转发至Transforming Lives, One Pound At A Time | Weight High并将约10%的流量转发至最佳欧洲杯赔率表,投注必赢策略分享 - 2024年欧洲足球锦标赛

    routes:
       -id: weight_route1 
       uri: https://weighthigh.org
       predicates:
           -Path=/product/**
           -Weight=group3, 1
       -id: weight_route2 
       uri: https://weighlow.org 
       predicates:
           -Path=/product/**
           -Weight= group3, 9

五、过滤器

三个知识点:

  1. 作用: 过滤器就是在请求的传递过程中,对请求和响应做一些手脚

  2. 生命周期: Pre Post

  3. 分类: 局部过滤器(作用在某一个路由上) 全局过滤器(作用全部路由上)

在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。

  1. PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。

  2. POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。

Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。

  1. GatewayFilter:应用到单个路由或者一个分组的路由上。

  2. GlobalFilter:应用到所有的路由上。

(一)网关过滤器(GatewayFilter)

局部过滤器:

spring:  
  cloud:  
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务
      routes:  # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
        - id: user_server  # 当前路由的标识, 要求唯一
          uri: lb://user-server  # 请求要转发到的地址
          order: 1  # 路由的优先级,数字越小级别越高
          predicates:  # 断言(就是路由转发要满足的条件)
            - Path=/user-server/**  # 当请求路径满足Path指定的规则时,才进行路由转发
          filters:  # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1  # 转发之前去掉1层路径

 

(二)全局过滤器(GlobalFilter)

全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。

1、自定义全局过滤器-token验证

内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。 开发中的鉴权逻辑:

  1. 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)

  2. 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证

  3. 以后每次请求,客户端都携带认证的token

  4. 服务端对token进行解密,判断是否有效。

如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验。 检验的标准就是请求中是否携带token凭证以及token的正确性。 下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。(将token存到radis缓存)

1.导redis的jar包

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

2.redis配置类

@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
​
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate= new RedisTemplate<>();
        // 可根据需要添加序列化器
        // template.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

3.redis工具类

/**
 * @Description: com.buba.utils   Redis工具类
 */
@Component
public final class RedisUtil {
​
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }
    // ============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    // ================================Map=================================
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
    // ============================set=============================
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================
    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     *
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
​
}

4.yml配置文件

server:
  port: 7002
spring:
  redis: # window
    port: 6379
    host: 127.0.0.1
  application:
    name: gateway-server
  cloud:
    nacos: # Linux
      discovery:
        username: nacos
        password: nacos
        server-addr: 192.168.177.129:8848
    gateway:
      discovery:
        locator:
          enabled: true

5.Token过滤器

@Component
public class TokenFilter implements GlobalFilter, Ordered {
​
    @Autowired
    private RedisUtil redisUtil;
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1、获取请求对象  ServerHttpRequest
        ServerHttpRequest request = exchange.getRequest();
        //2、获取请求的资源路径
        String path = request.getURI().getPath();
​
        //3、判断当前路径是否是不需要登录的资源路径—不需要则放过请求直接去执行目标接口
        //                                  —需要登录则如下判断token
        if(!"/user-server/user/login".equals(path) && !("/user-server/user/regist".equals(path))){
            //4、获取到请求头中的token
            List<String> tokens = request.getHeaders().get("token");
            String token = (tokens!=null&&tokens.size()>0)?tokens.get(0):null;
            //5、获取到请求头中的uid
            List<String> uids = request.getHeaders().get("uid");
            String uid = (uids!=null&&uids.size()>0)?uids.get(0):null;
            if(token!=null && uid!=null){
                //6、获取redis中的token
                String redis_token = String.valueOf(redisUtil.get("TOKEN_"+uid));
                //7、验证token是否有效
                if(redis_token==null|| "".equals(redis_token) || !redis_token.equals(token)){
                    //8、无效则返回错误相应
                    return onFailure(exchange.getResponse(),"token失效Q");
                }
​
            }else{
                //6、没有携带token返回错误相应
                return onFailure(exchange.getResponse(),"未登录,请先登录!");
            }
        }
​
        //去找执行目标方法
        return chain.filter(exchange);
    }
​
    @Override
    public int getOrder() {
        return 0;
    }
​
    public Mono<Void> onFailure(ServerHttpResponse response, String mes){
        JsonObject message = new JsonObject();
        message.addProperty("success", false);
        message.addProperty("code", 403);
        message.addProperty("data", mes);
        byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        //response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
}

6.测试

按正常来说登录的时候要在redis中设置redisUtil.set("TOKEN_"+uid,"用户id"),现在模拟在redis中set一个请求头信息 给111用户设置一个123456的token。

 

2、自定义全局过滤器-鉴权

1.在网关中判断当前用户是否有权限

import com.alibaba.nacos.shaded.com.google.gson.JsonObject;
import com.buba.feign.UserFeign;
import com.buba.utils.RedisUtil;
import feign.Feign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Lazy;
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.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
​
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
​
@Component
public class TokenFilter
        implements GlobalFilter, Ordered
        {
​
    @Autowired
    private RedisUtil redisUtil;
​
    @Lazy // 注意:注入使用懒加载,在gateway网关中不能使用openfeign同步调用,需要采取异步方式
    @Resource
    private UserFeign userFeign;
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1、获取请求对象  ServerHttpRequest
        ServerHttpRequest request = exchange.getRequest();
        //2、获取请求的资源路径
        String path = request.getURI().getPath();
        //5、获取到请求头中的uid
        List<String> uids = request.getHeaders().get("uid");
        String uid = (uids!=null&&uids.size()>0)?uids.get(0):null;
​
        //3、判断当前路径是否是不需要登录的资源路径—不需要则放过请求直接去执行目标接口
        //                                  —需要登录则如下判断token
        if(!"/user-server/user/login".equals(path) && !("/user-server/user/regist".equals(path))){
            //4、获取到请求头中的token
            List<String> tokens = request.getHeaders().get("token");
            String token = (tokens!=null&&tokens.size()>0)?tokens.get(0):null;
​
            if(token!=null && uid!=null){
                //6、获取redis中的token
                String redis_token = String.valueOf(redisUtil.get("TOKEN_"+uid));
                //7、验证token是否有效
                if(redis_token==null|| "".equals(redis_token) || !redis_token.equals(token)){
                    //8、无效则返回错误相应
                    return onFailure(exchange.getResponse(),"token失效Q");
                }
​
            }else{
                //6、没有携带token返回错误相应
                return onFailure(exchange.getResponse(),"未登录,请先登录!");
            }
        }
        //以下为新增内容根据用户id查询该用户拥有的资源接//
        if(uid!=null && !"".equals(uid) ){
            Long aLong = Long.valueOf(uid) ;
            // 注意:注入使用懒加载,在gateway网关中不能使用openfeign同步调用,需要采取异步方式
             // 异步调用feign服务接口
            CompletableFuture<List<String>> com =
                    CompletableFuture.supplyAsync(()->{
                        return userFeign.selectResByUid(aLong);
                    });
            List<String> u = null;
            try {
                u = com.get();
            }catch (Exception ex){
                ex.printStackTrace();
            }
            boolean b = u.contains(path);
            if(!b){
                return onFailure(exchange.getResponse(),"您没有该资源的访问权限!");
            }
        }
//        //去找执行目标方法
        return chain.filter(exchange);
    }
    @Override
    public int getOrder() {
        return 0;
    }
    public Mono<Void> onFailure(ServerHttpResponse response, String mes){
        JsonObject message = new JsonObject();
        message.addProperty("success", false);
        message.addProperty("code", 403);
        message.addProperty("data", mes);
        byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        //response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
}

上面代码中,网关过滤器通过Feign调用user服务中的方法(selectResByUid),去查找数据库中用户的访问资源,如果存在就放过,如果没有就返回。

2.使用Feign调用

1)准备Feign接口

@FeignClient(name = "user-server", path = "/user")
public interface UserFeign {
    @GetMapping("/getPaths")
    List<String> selectResByUid(@RequestParam("uid") Long uid);
}

2)启动类上加注解:@EnableFeignClients

3)user模块Controller层准备对应的方法,该控制器方法的存在,就是为了让网关过滤器调用

// 测试获取用户访问权限
@GetMapping("/getPaths")
public List<String> getPaths(@RequestParam("uid") Long uid){
    List<String> strings = userMapper.selectResByUid(uid);
    return strings;
}

3.这之后出现错误,解决办法

1)服务器启动转圈

Gateway服务器启动不成功,一直转圈,也不报错。在网关过滤器上自动装配Feign接口上加注解:@Lazy

 

2)报阻塞异常

在过滤类中正常调用feign服务接口时,会抛出一个java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2,意思是线程堵塞,使用CompletableFuture.supplyAsync异步调用解决。

 哪里使用Feign接口调用其他服务的控制器方法,那么就在哪里使用CompletableFuture.supplyAsync异步调用解决。

3)报空指针异常

不管怎么访问资源路径,Debug模式查看过滤器中Feign调用的方法获取到的结果一直为空:

加上Feign配置类,解决异步调用 feign 的错误。

import feign.Logger;
import feign.codec.Decoder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.util.ArrayList;
import java.util.List;
​
@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLevel() {
        //这里记录所有
        return Logger.Level.FULL;
    }
​
    @Bean
    public Decoder feignDecoder() {
        return new ResponseEntityDecoder(new SpringDecoder(feignHttpMessageConverter()));
    }
​
    public ObjectFactory<HttpMessageConverters> feignHttpMessageConverter() {
        final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(new PhpMappingJackson2HttpMessageConverter());
        return new ObjectFactory<HttpMessageConverters>() {
            @Override
            public HttpMessageConverters getObject() throws BeansException {
                return httpMessageConverters;
            }
        };
    }
​
    public class PhpMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
        PhpMappingJackson2HttpMessageConverter(){
            List<MediaType> mediaTypes = new ArrayList<>();
            mediaTypes.add(MediaType.valueOf(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8"));
            setSupportedMediaTypes(mediaTypes);
        }
    }
}

4.成功了

Navicat:表

Postman

访问数据库中存在的资源,则返回结果

如果访问的资源没有权限,则返回

六、网关限流

(一)接口限流

GateWay限流是基于Redis使用令牌桶算法实现的,所以要引入Redis依赖,以及配置Redis参数信息

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

使用Postman测试Spring Cloud Gateway的限流器有以下步骤:

  1. 启动Gateway与Redis服务器,确保限流器配置正确并生效。

  2. 找到限流器所在的路由地址与配置。例如: 配置限流的过滤器信息

server:
  port: 7002
spring:
  redis:
    port: 6379
    host: 127.0.0.1
  application:
    name: gateway-server
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: 192.168.177.129:8848
    gateway:
      routes:
        - id: user-server
          uri: lb://user-server
          predicates:
            - Path=/user-server/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                key-resolver: '#{@ipKeyResolver}'
                # 指定了令牌桶每秒填充速率,表示每秒钟可以放入的请求数量
                redis-rate-limiter.replenishRate: 1  
                # 指定了令牌桶的容量,即最大允许的瞬时并发请求数量。
                redis-rate-limiter.burstCapacity: 2   

该路由的地址为:/user-server/**,

filter 名称必须是 RequestRateLimiter。

限流参数为:

  • key-resolver:使用 SpEL 按名称引用 bean。 用于指定限流时使用的键解析器(Key Resolver)

@Configuration
public class AppCoonfig {
    @Bean
    public KeyResolver ipKeyResolver(){
​
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                return  Mono.just(exchange.getRequest().getPath().value());
            }
        };
    }
}
  • replenishRate: 1,每秒新增1个令牌

  • burstCapacity: 2,令牌桶最大容量2个令牌

  1. 点击"Send"按钮,观察响应结果。

    如果返回200状态码,表示此时令牌桶中还有令牌,请求被放行。

    如果返回429状态码,表示令牌桶中令牌不足,请求被限流。

  2. 在1秒内反复点击"Send"按钮,当触发限流时会出现429响应。

  3. 此时停止点击1秒,等待令牌桶填充新的1个令牌。(也就是等待2秒,桶就填充满了)

  4. 再次点击"Send"按钮,会再次得到200响应,表示新的令牌已填充。


(二)Gateway整合Sentinel实现网关限流

1.网关如何限流?

从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:

  • route维度:即在配置文件中配置的路由条目,资源名为对应的routeId,这种属于粗粒度的限流,一般是对某个微服务进行限流。

  • 自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组,这种属于细粒度的限流,针对某一类的uri进行匹配限流,可以跨多个微服务。

Spring Cloud Gateway集成Sentinel实现很简单,这就是阿里的魅力,提供简单、易操作的工具,让程序员专注于业务。

2.实战演示

1)gateway模块,添加如下依赖:

<!--nacos注册中心的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Gateway的依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--sentinelH整合gateway的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!--sentinel的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

注意:这依然是一个网关服务,不要添加WEB的依赖

2)yml配置文件

配置文件中主要指定以下三种配置:

  • nacos的地址

  • sentinel控制台的地址

  • 网关路由的配置

server:
  port: 7002
spring:
  application:
    name: gateway-server
  redis:
    port: 6379
    host: 127.0.0.1
  cloud:
    # 开始gateway的配置
    gateway:
      routes:
        - id: user_server
          uri: lb://user-server
          predicates:
            - Path=/user-server/**
          filters:
            - StripPrefix=1
    # nacos的配置
    nacos:
      discovery:
        server-addr: 192.168.177.129:8848  # nacos注册地址
        username: nacos
        password: nacos
    # sentinel配置
    sentinel:
      transport:
        # 指定sentinel控制台远程地址
        dashboard: 192.168.177.129:8081
        port: 8719
      # 直接建立心跳
      eager: true
      scg:
        # 限流后的响应配置
        fallback:
          content-type: application/json
          # 模式 response、redirect
          mode: response
          # 响应状态码
          response-status: 429
          # 响应信息
          response-body: 对不起,已经被限流了!!!

上述配置中设置了一个路由user_server,只要请求路径满足/user_server/**都会被路由到user_server这个服务中。

3)限流配置

经过上述两个步骤其实已经整合好了Sentinel,此时访问sentinel控制台,

然后在sentinel控制台可以看到已经被监控了,监控的路由是user_server,如下图:

 此时我们可以为其新增一个route维度的限流,如下图:

 

上图中对user-server这个路由做出了限流,QPS阈值为1。

此时快速访问:http://localhost:7002/user-server/user/get6,看到已经被限流了,如下图:

以上route维度的限流已经配置成功,小伙伴可以自己照着上述步骤尝试一下。  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/601449.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linuxOPS基础_用户与组管理

linux用户与组概念 为什么需要了解用户和组 服务器要添加多账户的作用 ​ 针对不同用户分配不同的权限&#xff0c;不同权限可以限制用户可以访问到的系统资源 ​ 提高系统的安全性 ​ 帮助系统管理员对使用系统的用户进行跟踪 用户和组的关系 理论上Linux系统中的每个用户…

2023年6月实时获取地图边界数据方法,省市区县街道多级联动【附实时geoJson数据下载】

首先&#xff0c;来看下效果图 在线体验地址&#xff1a;https://geojson.hxkj.vip&#xff0c;并提供实时geoJson数据文件下载 可下载的数据包含省级geojson行政边界数据、市级geojson行政边界数据、区/县级geojson行政边界数据、省市区县街道行政编码四级联动数据&#xff0…

日本原装Yokogawa AQ6317B横河AQ6317C光谱分析仪

Yokogawa AQ6317B光谱分析仪&#xff0c;50 GHz ​Yokogawa AQ6317B 光谱分析仪 (OSA) 是一款先进的光谱分析仪&#xff0c;应用范围广泛&#xff0c;包括光源评估、光学设备损耗波长特性的测量以及 WDM&#xff08;波分复用&#xff09;系统的波形分析。在 Yokogawa 购买产品…

第十七篇、基于Arduino uno,获取cp2d12红外测距传感器的原始值和距离值——结果导向

0、结果 说明&#xff1a;先来看看串口调试助手显示的结果&#xff0c;第一个值是原始的模拟电压值&#xff0c;第二个值是距离值&#xff0c;如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;虽然红外测距传感器形态各异&#xff0c;但是原理和代码都是…

java中实现对象属性复制的工具类

在 Java 中&#xff0c;有多个工具类可用于实现对象属性的复制&#xff0c;使得属性值从一个对象复制到另一个对象。以下是几个常用的工具类&#xff1a; Apache Commons BeanUtils&#xff1a; Apache Commons BeanUtils 提供了 BeanUtils 类&#xff0c;可以方便地进行属性…

一文简介Linux固件子系统的实现机制

一、Linux固件子系统概述 固件是硬件设备自身执行的一段程序。固件一般存放在设备flash内。而出于成本和便利性的考虑&#xff0c;通常是先将硬件设备的运行程序打包为一个特定格式的固件文件&#xff0c;存储到终端系统内&#xff0c;通过终端系统给硬件设备进行升级。Linux内…

java面向对象学习

一、Java类及类的成员 1.类是对一类事物的描述&#xff0c;是抽象的、概念上的定义 2.对象是实际存在的该类事物的每个个体&#xff0c;因而也称为实例 3.属性&#xff1a;对应类中的成员变量 4.行为&#xff1a;对应类中的方法 权限修饰符号&#xff1a;public、protected…

玄派玄智星笔记本U盘重装电脑系统详细步骤教学

玄派玄智星笔记本U盘重装电脑系统详细步骤教学。有用户使用玄派玄智星笔记本的时候&#xff0c;电脑系统出现了故障&#xff0c;导致自己无法启动电脑了。这个情况需要使用U盘去进行系统的重装&#xff0c;那么具体要怎么去进行重装呢&#xff1f;来看看以下的操作方法吧。 准备…

移动端布局之流式布局1(百分比布局):流式布局基础、案例:京东移动端首页1

移动端布局之流式布局1 流式布局&#xff08;百分比布局&#xff09;基础案例&#xff1a;京东移动端首页搭建相关文件夹结构设置视口标签以及引入初始化样式normalize.css引入我们的css初始化文件与首页css body设置index.css app布局和app内容填充index.htmlindex.css 搜索模…

小说App源码分享,从零开始搭建小说阅读平台

作为一名小说阅读爱好者或者创业者&#xff0c;你是否也曾经想要搭建自己的小说阅读平台&#xff1f;然而&#xff0c;开发一款小说App通常需要大量的人力、物力和时间成本&#xff0c;怎样才能让它变得更加容易&#xff1f;今天&#xff0c;我将与大家分享如何从零开始&#x…

VSD?啥是VSD?VSD应用场景你知道吗?

软件介绍 Vayo-Stencil Designer Vayo-Stencil Designer&#xff08;简称VSD&#xff09;是一款面向企业的专业钢网设计软件&#xff0c;可以为企业高效构建适合企业自身产品和工艺know-how的数字化开口规范&#xff0c;解决钢网开口审查、局部开口设计、完整钢网设计、PIP焊…

07 【内置指令 自定义指令】

1. 内置指令 之前学过的指令&#xff1a; v-bind 单向绑定解析表达式&#xff0c;可简写为 :v-model 双向数据绑定v-for 遍历数组 / 对象 / 字符串v-on 绑定事件监听&#xff0c;可简写为****v-show 条件渲染 (动态控制节点是否展示)v-if 条件渲染&#xff08;动态控制节点是…

一文读懂责任分配矩阵,解决你80%的项目难题

成功的项目管理取决于整个团队对角色和职责的理解&#xff0c;使用责任分配矩阵分配和定义角色是使项目保持在正轨并为成功做好准备的好方法。 如果设计得当&#xff0c;责任分配矩阵能够促进项目的成功交付。 一、什么是责任分配矩阵 责任分配&#xff08;RACI&#xff09;矩…

行驶的汽车-第14届蓝桥杯国赛Scratch真题初中级组第1题

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第143讲。 行驶的汽车&#xff0c;本题是2023年5月28日上午举行的第14届蓝桥杯国赛Scratch图形化编程初中级组真题第1题…

chatgpt赋能python:如何关闭Python中的Figure?

如何关闭Python中的Figure&#xff1f; 简介 在Python中使用Matplotlib生成图形时&#xff0c;我们会使用到Figure对象&#xff0c;它是图形的容器。在一些情况下&#xff0c;我们可能需要手动关闭这个Figure&#xff0c;例如多次运行程序导致Figure叠加、或者让程序周期性的…

Java程序设计入门教程--字符类String

String构造方法 创建字符串有两种格式 String 字符串名 new String &#xff08;字符串常量&#xff09; ; String 字符串名 字符串常量 ; String str new String ( "student" ); String str "student"&#xff1b;两种格式的区别 这两种格式生成…

配置WordPress主题时RESTAPI问题

问题1&#xff1a; session_start()函数调用生成了一个会话.该会话干扰了RESTAPI及环回请求。在做出任何HTTP请求前&#xff0c;该会话必须由session_write_close()函数关闭. 问题2&#xff1a; RESTAPI是WordPress及其他应用与服务器通信的一种途径。例如区块编辑器页面&am…

93年的测试人,什么也不会敢要12K!思绪万千..

前不久&#xff0c;公司面试了一个93年的测试人&#xff0c;听同事说&#xff0c;在IT行业也摸爬滚打很多年了&#xff0c;现在从事测试岗位&#xff0c;可是什么也不会&#xff0c;却开口说要1.2w.其实挺佩服他的勇气。同事表示开始对他还挺满意的&#xff0c;但是中间发现他包…

【离散数学】群论考核回顾

写在前面&#xff1a; 1&#xff1a;本文依然不回顾小题的具体题目&#xff0c;此次考试的小题多为二级结论&#xff0c;且全卷基本上没考陪集后面的知识点。小题较多&#xff0c;耗时可能会较大&#xff0c;反正我差点没做完卷子&#xff08;排除完全没思路的题&#xff09;。…

EWM是什么,需要了解什么

EWM是SAP的一个模块&#xff0c;代表扩展仓库管理&#xff08;Extended Warehouse Management&#xff09;&#xff0c;是SAP企业资源计划&#xff08;ERP&#xff09;的一部分。它提供了一个完整的、高级的仓库管理解决方案&#xff0c;支持企业在全球范围内的仓库管理、订单管…