🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践

简介: 本文介绍了如何使用Spring Cloud Alibaba 2023.0.0.0技术栈构建微服务网关,以应对微服务架构中流量治理与安全管控的复杂性。通过一个包含鉴权服务、文件服务和主服务的项目,详细讲解了网关的整合与功能开发。首先,通过统一路由配置,将所有请求集中到网关进行管理;其次,实现了限流防刷功能,防止恶意刷接口;最后,添加了登录鉴权机制,确保用户身份验证。整个过程结合Nacos注册中心,确保服务注册与配置管理的高效性。通过这些实践,帮助开发者更好地理解和应用微服务网关。

引子

当前微服务架构已成为中大型系统的标配,但在享受拆分带来的敏捷性时,流量治理与安全管控的复杂度也呈指数级上升。因此,我们需要构建微服务网关来为系统“保驾护航”。本文将会通过一个项目(核心模块包含 鉴权服务、文件服务、主服务 共 3 个微服务),采用 Spring Cloud Alibaba 2023.0.0.0 版本技术栈(核心组件:Nacos 2.5.0 注册中心与配置中心),分享如何构建一个微服务网关。

为什么需要微服务网关

我们当前模拟的这个项目中包含了三个业务服务,如果部署到线上的话,每个服务都有自己的ip(或域名)以及端口号。因此,我们的业务入口是分散的且暴露在外的,我们无法统一拦截异常流量以及限制接口访问等。但有了微服务网关,我们就可以将所有的请求都先集中在网关这里(有点类似于一个房子的大门口),由网关对所有请求进行统一的管理。

1.png

实践

在知晓了网关的作用后,我们将实践如何在一个现成的微服务项目中整合gateway网关以及做功能开发。当然,在这之前,我们需要先完成整合。首先,我们需要建一个网关模块,如下:

2.png

完成模块的创建后,导入gateway相关的依赖,如下:

    <dependencies>
        <dependency>
            <groupId>com.pitayafruits</groupId>
            <artifactId>wechat-pojo</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
    </dependencies>

说明一下:这里引入的pojo包含了项目中常用的方法、工具类等;web则是因为网关本身也是一个可以访问的服务,所以需要引入;gateway则是这里需要使用的网关的依赖。然后来对它进行基础的配置,如下:

server:
  port: 1000
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1 # 不限制请求体大小

spring:
  application:
    name: gateway
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        username: nacos
        password: naocs

# 日志级别
logging:
  level:
    root: info

我们使用了nacos来管理服务,网关自然也是一个服务,因此也需要把它注册到nacos

1.统一路由

引入网关的首要作用是统一访问的入口,所有的服务访问都要先经过网关。因此,第一个要实现的功能就是统一路由。而它的实现也是非常简单,只需要在配置文件中做下简单配置即可:

spring:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:           # 路由配置信息(数组/list)
        - id: authRoute # 每项路由规则都有一个唯一的id编号,可以自定义
          uri: lb://auth-service # lb=负载均衡,会动态寻址
          predicates:
            - Path=/a/**
        - id: fileRoute
          uri: lb://file-service
          predicates:
            - Path=/f/**
        - id: mainRoute
          uri: lb://main-service
          predicates:
            - Path=/m/**
      globalcors: # 允许跨域的相关配置
        cors-configurations:
          '[/**]':
            allowedOriginPatterns: "*"
            allowedHeaders: "*"
            allowedMethods: "*"
            allowCredentials: true

这里对routes下的相关配置说明下:id是给每个服务的路由一个唯一编号,保证唯一即可,通常我们采用的写法是服务名+route;uri则是服务名称,如果写成ip或者域名,那么地址发生变化,我们还需要重新修改配置,但是服务名称是可以固定不变的;接下来是predicates,它可以配置多个值,我们一个服务里会有多个controller,把每个controller的路由配置在这里即可,/**表示指定的controller下的所有方法。

另外,如果负载均衡这个写法无法被识别,说明你当前使用的spring-cloud版本中默认并不包含相关依赖,我们需要手动引入它。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

完成上述配置后,我们此时其他服务的API将无法直接访问,而统一通过网关来访问。例如原本main-service中的127.0.0.1:88/m/hello 变成了 127.0.0.1:1000/m/hello

2.限流防刷

提到网关,一个绕不开的话题就是限流。如果有人恶意刷我们的接口,我们就需要对某些IP进行访问限制,比如在XX秒内访问同一接口超过XX次,就需要限制访问。它的实现非常简单,声明一个处理类继承gateway的相关过滤接口即可。代码如下:

@Component
public class IPLimitFilter implements GlobalFilter {
   

    private static final Integer continueCounts = 3;

    private static final Integer timeInterval = 20;

    private static final Integer limitTimes = 30;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   
        return doLimit(exchange, chain);
    }

    /**
     * 限制ip请求次数的判断
     *
     * @param exchange 请求交换器
     * @param chain     过滤器链
     * @return 返回值
     */
    public Mono<Void> doLimit(ServerWebExchange exchange,
                              GatewayFilterChain chain) {
   
        // 获取ip
        ServerHttpRequest request = exchange.getRequest();
        String ip = IPUtil.getIP(request);

        // 正常ip定义
        final String ipRedisKey = "gateway-ip" + ip;
        // 被拦截的黑名单,如果在redis中存在,那么就不允许访问
        final String ipRedisLimitKey = "gateway-ip:limit" + ip;

        // 判断当前ip的剩余时间,如果大于0,则表示还处于黑名单
        long limitLeftTimes = redis.ttl(ipRedisLimitKey);
        if ( limitLeftTimes > 0 ) {
   
            return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
        }

        // 在redis中更新次数
        long requestCounts = redis.increment(ipRedisKey, 1);
        // 如果第一次访问,就需要设置间隔时间
        if (requestCounts == 1) {
   
            redis.expire(ipRedisKey, timeInterval);
        }
        // 如果还能获得正常请求次数,说明用户的正常请求落在正常时间内,超过则限制
        if (requestCounts > continueCounts) {
   
            redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes);
            return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
        }
        // 放行请求
        return chain.filter(exchange);
    }

    //过滤器的顺序,数字越小优先级越大.
    @Override
    public int getOrder() {
   
        return 1;
    }

我们需要借助redis来实现根据时间对指定ip的控制,这里的逻辑是:如果某个ip在30秒访问超过三次,就限制访问,如果限制了,则20秒后再恢复。

3.登录鉴权

关于登录鉴权,我们目前通常会采用无状态的做法:即用户登录后,后端返回token给前端,前端后续所有的请求都在headers中携带token,后端服务不存储token,只对前端发来的token进行校验和解析。而网关作为所有服务的入口,自然而然地也就可以承担起这个职责了。

import com.google.gson.Gson;
import com.pitayafruits.base.BaseInfoProperties;
import com.pitayafruits.grace.result.GraceJSONResult;
import com.pitayafruits.grace.result.ResponseStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.context.config.annotation.RefreshScope;
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.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

@Component
@Slf4j
@RefreshScope
public class SecurityFilterToken extends BaseInfoProperties implements GlobalFilter, Ordered {
   

    @Resource
    private ExcludeUrlProperties excludeUrlProperties;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   

        // 获取用户请求路径
        String url = exchange.getRequest().getURI().getPath();

        // 获取所有需要排除校验的url
        List<String> excludeList = excludeUrlProperties.getUrls();

        // 校验并排除url
        if (excludeList != null && !excludeList.isEmpty()) {
   
            for (String excludeUrl : excludeList) {
   
                if (antPathMatcher.matchStart(excludeUrl, url)) {
   
                    return chain.filter(exchange);
                }
            }
        }
        // 从header中获得用户id和token
        String userId = exchange.getRequest().getHeaders().getFirst(HEADER_USER_ID);
        String userToken = exchange.getRequest().getHeaders().getFirst(HEADER_USER_TOKEN);

        // 校验header中的token
        if (StringUtils.isNotBlank(userId) && StringUtils.isNotBlank(userToken)) {
   
            String redisToken = redis.get(REDIS_USER_TOKEN + ":" + userId);
            if (redisToken.equals(userToken)) {
   
                return chain.filter(exchange);
            }
        }

        // 默认不放行
        return renderErrorMsg(exchange, ResponseStatusEnum.UN_LOGIN);
    }

    //过滤器的顺序,数字越小优先级越大.
    @Override
    public int getOrder() {
   
        return 0;
    }

    /**
     * 异常信息包装
     *
     * @param exchange   交换器
     * @param statusEnum 状态枚举
     * @return 返回值
     */
    public Mono<Void> renderErrorMsg(ServerWebExchange exchange,
                                     ResponseStatusEnum statusEnum) {
   
        //1.获得response
        ServerHttpResponse response = exchange.getResponse();
        //2.构建jsonResult
        GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);
        //3.设置header类型
        if (!response.getHeaders().containsKey("Content-Type")) {
   
            response.getHeaders().add("Content-Type",
                    MimeTypeUtils.APPLICATION_JSON_VALUE);
        }
        //4.设置状态码
        response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
        //5.转换json并向response写数据
        String resultJson = new Gson().toJson(jsonResult);
        DataBuffer buffer = response.bufferFactory().wrap(resultJson.getBytes(StandardCharsets.UTF_8));
        //6.返回
        return response.writeWith(Mono.just(buffer));
    }
}

在我这个示例中,我做的校验逻辑很简单:只是用户登录的时候会在redis里存放生成的token,然后其他接口访问的时候比对下传来的tokenredis里存放的token是否一致。这里需要关注下过滤器的顺序,目前的案例中我们已经编写了两个过滤器-限流防刷和登录鉴权。所以可以把登录鉴权过滤器的执行顺序改为0,限流防抖改为1。

另外,我们需要对部分接口放行不拦截,比如登录接口。而我这里的做法则是将放行接口写在配置文件里,并声明配置类进行读取。

exclude.urls[0] = /passport/getSMSCode
exclude.urls[1] = /passport/regist
exclude.urls[2] = /passport/login
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@Data
@PropertySource("classpath:excludeUrlPath.properties")
@ConfigurationProperties(prefix = "exclude")
public class ExcludeUrlProperties {
   

    private List<String> urls;

}

特别说明下:这里制定好过滤器的执行顺序后,内部的验证逻辑根据自己实际情况填写,我这里没用鉴权框架只是方便讲解,要用也很简单,引入之后把相关的鉴权逻辑写进对应的过滤器就行。

小结

在本文中,我们完成了Spring Cloud Gateway微服务网关的整合,并完成了三个最基础常见的实践场景。如果你的项目有更多的业务需求,只需要加相应的过滤器并制定好过滤器的执行顺序即可,希望对大家有所帮助!

目录
相关文章
|
9天前
|
JSON 前端开发 Java
深入理解 Spring Boot 中日期时间格式化:@DateTimeFormat 与 @JsonFormat 完整实践
在 Spring Boot 开发中,日期时间格式化是前后端交互的常见痛点。本文详细解析了 **@DateTimeFormat** 和 **@JsonFormat** 两个注解的用法,分别用于将前端传入的字符串解析为 Java 时间对象,以及将时间对象序列化为指定格式返回给前端。通过完整示例代码,展示了从数据接收、业务处理到结果返回的全流程,并总结了解决时区问题和全局配置的最佳实践,助你高效处理日期时间需求。
74 0
|
9天前
|
存储 Java 数据库
Spring Boot 注册登录系统:问题总结与优化实践
在Spring Boot开发中,注册登录模块常面临数据库设计、密码加密、权限配置及用户体验等问题。本文以便利店销售系统为例,详细解析四大类问题:数据库字段约束(如默认值缺失)、密码加密(明文存储风险)、Spring Security配置(路径权限不当)以及表单交互(数据丢失与提示不足)。通过优化数据库结构、引入BCrypt加密、完善安全配置和改进用户交互,提供了一套全面的解决方案,助力开发者构建更 robust 的系统。
32 0
|
8天前
|
Java 开发者 微服务
Spring Cloud OpenFeign详解与实践
总结起来说,Spring Cloud OpenFeign提供了一种简单易懂且高效的方式去实现微服务之间通信.它隐藏了许多复杂性,并且允许开发者以声明式方式编写HTTP客户端代码.如果你正在开发基于Spring Cloud 的微服务架构系统,Spring Cloud Open Feign是一个非常好用且强大工具.
101 33
|
3天前
|
存储 负载均衡 测试技术
ACK Gateway with Inference Extension:优化多机分布式大模型推理服务实践
本文介绍了如何利用阿里云容器服务ACK推出的ACK Gateway with Inference Extension组件,在Kubernetes环境中为多机分布式部署的LLM推理服务提供智能路由和负载均衡能力。文章以部署和优化QwQ-32B模型为例,详细展示了从环境准备到性能测试的完整实践过程。
|
10天前
|
JSON 前端开发 Java
深入理解 Spring Boot 中日期时间格式化:@DateTimeFormat 与 @JsonFormat 完整实践
在 Spring Boot 开发中,处理前后端日期交互是一个常见问题。本文通过 **@DateTimeFormat** 和 **@JsonFormat** 两个注解,详细讲解了如何解析前端传来的日期字符串以及以指定格式返回日期数据。文章从实际案例出发,结合代码演示两者的使用场景与注意事项,解决解析失败、时区偏差等问题,并提供全局配置与局部注解的实践经验。帮助开发者高效应对日期时间格式化需求,提升开发效率。
148 2
|
1月前
|
存储 人工智能 Kubernetes
ACK Gateway with AI Extension:面向Kubernetes大模型推理的智能路由实践
本文介绍了如何利用阿里云容器服务ACK推出的ACK Gateway with AI Extension组件,在Kubernetes环境中为大语言模型(LLM)推理服务提供智能路由和负载均衡能力。文章以部署和优化QwQ-32B模型为例,详细展示了从环境准备到性能测试的完整实践过程。
|
1月前
|
存储 人工智能 物联网
ACK Gateway with AI Extension:大模型推理的模型灰度实践
本文介绍了如何使用 ACK Gateway with AI Extension 组件在云原生环境中实现大语言模型(LLM)推理服务的灰度发布和流量分发。该组件专为 LLM 推理场景设计,支持四层/七层流量路由,并提供基于模型服务器负载感知的智能负载均衡能力。通过自定义资源(CRD),如 InferencePool 和 InferenceModel,可以灵活配置推理服务的流量策略,包括模型灰度发布和流量镜像。
|
2月前
|
Cloud Native Java Nacos
springcloud/springboot集成NACOS 做注册和配置中心以及nacos源码分析
通过本文,我们详细介绍了如何在 Spring Cloud 和 Spring Boot 中集成 Nacos 进行服务注册和配置管理,并对 Nacos 的源码进行了初步分析。Nacos 作为一个强大的服务注册和配置管理平台,为微服务架构提供
481 14
|
关系型数据库 Java 数据格式
Spring 实践 -拾遗
Spring 实践 标签: Java与设计模式 Junit集成 前面多次用到@RunWith与@ContextConfiguration,在测试类添加这两个注解,程序就会自动加载Spring配置并初始化Spring容器,方便Junit与Spring集成测试.
1120 0
|
29天前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
51 0

热门文章

最新文章