当前位置: 代码网 > it编程>编程语言>Java > Spring Boot 3 整合 Spring Cloud Gateway实践过程

Spring Boot 3 整合 Spring Cloud Gateway实践过程

2025年02月26日 Java 我要评论
引子当前微服务架构已成为中大型系统的标配,但在享受拆分带来的敏捷性时,流量治理与安全管控的复杂度也呈指数级上升。因此,我们需要构建微服务网关来为系统“保驾护航”。本文将会通过一

引子

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

为什么需要微服务网关

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

实践

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

完成模块的创建后,导入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微服务网关的整合,并完成了三个最基础常见的实践场景。如果你的项目有更多的业务需求,只需要加相应的过滤器并制定好过滤器的执行顺序即可,希望对大家有所帮助!

到此这篇关于spring boot 3 整合 spring cloud gateway实践过程的文章就介绍到这了,更多相关spring boot 整合 spring cloud gateway 内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com