SpringCloudGateway过滤器(全局认证、IP拦截、请求参数过滤、响应参数过滤)

这篇具有很好参考价值的文章主要介绍了SpringCloudGateway过滤器(全局认证、IP拦截、请求参数过滤、响应参数过滤)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

全局过滤器(默认对所有路由生效)

全局过滤器(认证)

/**
 * @描述: TODO 全局认证过滤器,不需要在配置文件中配置,作用于所有的请求
 * @作者: lixing
 * @日期 2021/6/17 9:06
 */
@Component
public class MyAuthorizeGlobalFilter implements GlobalFilter, Ordered {
    /** 读取配置文件中的数据 */
    @Resource
    private CustomProperties customProperties;

    /** redis持久化工具类 */
    @Resource
    private RedissonSingleService<String> redissonSingleServiceString;

    /**
     * 过滤器执行的顺序,值越小,执行顺序越靠前
     */
    @Override
    public int getOrder() {
        return GatewayFilterOrderEnum.MY_AUTHORIZE.getValue();
    }

    /**
     * TODO 全局"前置"过滤器逻辑
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        ServerHttpResponse serverHttpResponse = exchange.getResponse();

        // 原始请求地址: 127.0.0.1:8800/springCloudGateway/auth/login/doLogin
        // 转化后的地址: /bffAuth/login/doLogin
        String reqPath = serverHttpRequest.getURI().getPath();

        // 全局白名单地址,例如:注册、登陆、忘记密码发送短信等等
        List<String> whiteUrlList = customProperties.getSys().getWhiteUrlList();

        /*
         * 请求白名单过滤
         */
        //   /login/**      匹配 /login 开头
        //   /trip/api/*x   匹配 /trip/api/x,/trip/api/ax,/trip/api/abx ;但不匹配 /trip/abc/x
        //   /trip/a/a?x    匹配 /trip/a/abx;但不匹配 /trip/a/ax,/trip/a/abcx
        //   /**/api/alie   匹配 /trip/api/alie,/trip/dax/api/alie;但不匹配 /trip/a/api
        //   /**/*.htmlm    匹配所有以.htmlm结尾的路径
        PathMatcher pathMatcher = new AntPathMatcher();
        for (String authIgnoreTemp : whiteUrlList) {
            if (StrUtil.isEffective(authIgnoreTemp) && pathMatcher.match(authIgnoreTemp, reqPath)) {
                LoggerUtil.info(MessageFormat.format("MyAuthorizeGlobalFilter 全局认证过滤器 ---- 地址无需登陆:{0}",reqPath));
                return chain.filter(exchange);
            }
        }
        LoggerUtil.info(MessageFormat.format("MyAuthorizeGlobalFilter 全局认证过滤器 ---- 地址需要登陆:{0}",reqPath));
        LoggerUtil.info("");

        /*
         * 对请求进行拦截并校验TOKEN
         */
        String token = serverHttpRequest.getHeaders().getFirst(SysKeyEnum.TOKEN.getKey());
        // 非空校验
        if (!StrUtil.isEffective(token)) {
            // 拦截,提示未授权错误,401
            //serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
            // 拦截结束请求
            //return serverHttpResponse.setComplete();
            return MonoResponseUtil.getVoidMono(serverHttpResponse, new BaseResult<String>().fail("TOKEN缺失"));
        }

        // 校验token,获取用户的登录对象
        try {
            BaseLoginUserInfo baseLoginUserInfo = JwtUtil.verifyJwtForHs256(token);

            // redis中校验登录状态
            String versionKey = MessageFormat.format(SysKeyEnum.LOGIN_USER_VERSION.getKey(), SysKeyConstant.PROJECTNAME, baseLoginUserInfo.getUserAccount());
            if (!redissonSingleServiceString.isExists(versionKey)) {
                // redis中没有当前key,则表示登录超时了,需要重写登录
                return MonoResponseUtil.getVoidMono(serverHttpResponse, new BaseResult<String>().fail(SysStateEnum.RESPONSE_STATUS_FALSE_TOKEN_TIMEOUT.getDescribe()));
            } else {
                // 判断当前的version和redis中的version是否一致,不一致则表示当前已经有人登录,需要重新登录
                String tokenVersion = redissonSingleServiceString.getKey(versionKey);
                if (!baseLoginUserInfo.getVersion().equals(tokenVersion)) {
                    return MonoResponseUtil.getVoidMono(serverHttpResponse, new BaseResult<String>().fail(SysStateEnum.RESPONSE_STATUS_FALSE_TOKEN_EXISTS.getDescribe()));
                }
            }

            // 修改请求求
            Consumer<HttpHeaders> httpHeaders = httpHeader -> {
                httpHeader.set(SysKeyEnum.USERINFO_ACCOUNT.getKey(), baseLoginUserInfo.getUserAccount());
                httpHeader.set(SysKeyEnum.USERINFO_PHONE.getKey(), baseLoginUserInfo.getUserAccount());
                httpHeader.remove(SysKeyEnum.TOKEN.getKey());
            };
            // 从原始交换对象获得一个新的 ServerHttpRequest 实例
            ServerHttpRequest serverHttpRequestMutable = serverHttpRequest.mutate().headers(httpHeaders).build();
            // 从原始交换对象获得一个新的 ServerWebExchange 实例
            ServerWebExchange serverWebExchangeMutable = exchange.mutate().request(serverHttpRequestMutable).build();
            //
            return chain.filter(serverWebExchangeMutable).then(Mono.fromRunnable(() -> {
                // TODO 全局"后置"过滤器逻辑
                LoggerUtil.info(MessageFormat.format("MyAuthorizeGlobalFilter 全局认证过滤器 ---- 全局后置过滤器回调逻辑:{0}",reqPath));
            }));
        } catch (Exception e) {
            return MonoResponseUtil.getVoidMono(serverHttpResponse, new BaseResult<String>().fail(e.getMessage()));
        }
    }
}

网关过滤器(局部过滤器,需要为相关路由配置才可生效)

IP网关过滤器

1、创建IP网关过滤器
/**
 * @描述: TODO IP网关过滤器,需要为相关路由配置上才可生效
 * @作者: lixing
 * @日期 2021/6/24 10:25
 */
public class MyIpGatewayFilter implements GatewayFilter, Ordered {
    private CustomProperties customProperties;
    private MyIpGatewayFilterFactoryConfig myIpGatewayFilterFactoryConfig;
    MyIpGatewayFilter(CustomProperties customProperties, MyIpGatewayFilterFactoryConfig myIpGatewayFilterFactoryConfig) {
        this.customProperties = customProperties;
        this.myIpGatewayFilterFactoryConfig = myIpGatewayFilterFactoryConfig;
    }

    /** 过滤器执行的顺序,值越小,执行顺序越靠前 */
    @Override
    public int getOrder() {
        return GatewayFilterOrderEnum.MY_IP.getValue();
    }

    /** 过滤 */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if ("*".equalsIgnoreCase(myIpGatewayFilterFactoryConfig.getWhiteIp())) {
            // 对所有ip放行
            return chain.filter(exchange);
        } else {
            ServerHttpRequest serverHttpRequest = exchange.getRequest();
            ServerHttpResponse serverHttpResponse = exchange.getResponse();
            // 请求地址
            String reqPath = serverHttpRequest.getURI().getPath();
            // 当前请求的ip
            String reqIp = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
            // 公共白名单列表
            List<String> whiteIpList = customProperties.getSys().getWhiteIpList();
            // 局部白名单列表
            List<String> configIpList = Arrays.asList(myIpGatewayFilterFactoryConfig.getWhiteIp().split(","));
            //
            if (whiteIpList.contains(reqIp) || configIpList.contains(reqIp)) {
                LoggerUtil.info(MessageFormat.format("MyIpGatewayFilter IP网关过滤器 ---- IP无需拦截:{0}", reqIp));
                LoggerUtil.info("");
                return chain.filter(exchange);
            }
            return MonoResponseUtil.getVoidMono(serverHttpResponse, new BaseResult<String>().fail("您还没有权限访问,请联系管理员"));
        }
    }
}
2、创建过滤器参数配置类
/**
 * @描述: TODO IP网关过滤器参数配置类
 * @作者: lixing
 * @日期 2021/6/25 9:22
 */
@Data
@NoArgsConstructor
public class MyIpGatewayFilterFactoryConfig {
    /** 默认白名单ip列表,*:表示对所有ip放行,指定ip列表的格式为:127.0.0.1,192.168.1.110 */
    private String whiteIp;
}
3、创建过滤器工厂
/**
 * @描述: TODO IP网关过滤器工厂,需要为相关路由配置上才可生效
 * @作者: lixing
 * @日期 2021/6/24 10:25
 */
@Component
public class MyIpGatewayFilterFactory extends AbstractGatewayFilterFactory<MyIpGatewayFilterFactoryConfig> {
    @Resource
    private CustomProperties customProperties;

    public MyIpGatewayFilterFactory() {
        super(MyIpGatewayFilterFactoryConfig.class);
    }

    /** 自定义配置类-内部类,用于传入配置参数 */
    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("whiteIp");
    }

    /**
     * 过滤器逻辑
     */
    @Override
    public GatewayFilter apply(MyIpGatewayFilterFactoryConfig config) {
        return new MyIpGatewayFilter(customProperties, config);
    }
}
4、配置IP网关过滤器
# TODO 基于 【BFF-AUTH】 服务的路由配置
- id: BFF-AUTH-ROUTE
  uri: lb://bffAuth
  predicates:
    - Path=/springCloudGateway/auth/**
  filters:
    # 自定义 [MyIpGatewayFilter] 网关过滤器。*表示允许所有ip请求,其它格式为:127.0.0.1,192.168.0.110
    - MyIp=127.0.0.1,192.168.1.1

请求参数网关过滤器

1、 创建请求参数网关过滤器
/**
 * @描述: TODO 请求参数网关过滤器,需要为相关路由配置上才可生效
 * @作者: lixing
 * @日期 2021/6/24 10:25
 */
public class MyRequestParamsGatewayFilter implements GatewayFilter, Ordered {

    /** 通过构造函数初始化参数配置类 */
    private MyRequestParamsGatewayFilterFactoryConfig myRequestParamsGatewayFilterFactoryConfig;
    public MyRequestParamsGatewayFilter(MyRequestParamsGatewayFilterFactoryConfig myRequestParamsGatewayFilterFactoryConfig) {
        this.myRequestParamsGatewayFilterFactoryConfig = myRequestParamsGatewayFilterFactoryConfig;
    }

    /** 过滤器执行的顺序,值越小,执行顺序越靠前 */
    @Override
    public int getOrder() {
        return GatewayFilterOrderEnum.MY_REQUEST_PARAMS.getValue();
    }

    /**
     * TODO 网关“前置”过滤器逻辑
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //
        String schema = request.getURI().getScheme();
        if ((!"http".equalsIgnoreCase(schema) && !"https".equalsIgnoreCase(schema))) {
            return chain.filter(exchange);
        }
        // 请求地址
        String reqPath = request.getPath().toString();
        // 请求方式:POST
        String method = request.getMethodValue();
        // 媒体类型:浏览器以JSON形式对资源进行解析
        MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
        // 请求中的参数是否需要解密
        exchange.getAttributes().put("isDecrypt", myRequestParamsGatewayFilterFactoryConfig.getIsDecrypt());
        if (SysKeyEnum.GET.getKey().equalsIgnoreCase(method)) {
            /*
             * GET 请求
             */
            MultiValueMap<String, String> queryParams = request.getQueryParams();
            if (SysStateEnum.STATE_YES.getDescribe().equalsIgnoreCase(myRequestParamsGatewayFilterFactoryConfig.getIsDecrypt())) {
                LoggerUtil.info("解密成功");
            }
            return chain.filter(exchange)
                // TODO 网关“后置”过滤器逻辑
                .then(Mono.fromRunnable(() -> {
                    if (SysStateEnum.STATE_YES.getDescribe().equalsIgnoreCase(myRequestParamsGatewayFilterFactoryConfig.getIsPrint())) {
                        LoggerUtil.info("");
                        LoggerUtil.info(MessageFormat.format("MyRequestParamsGatewayFilter 入参全局过滤器 ---- start [GET]:{0}", reqPath));
                        if (SysStateEnum.STATE_YES.getDescribe().equalsIgnoreCase(myRequestParamsGatewayFilterFactoryConfig.getIsDecrypt())) {
                            LoggerUtil.info(MessageFormat.format("入参解密前: {0}", JSONObject.toJSONString(queryParams)));
                            LoggerUtil.info(MessageFormat.format("入参解密后: {0}", JSONObject.toJSONString(queryParams)));
                        } else {
                            LoggerUtil.info(MessageFormat.format("入参: {0}", JSONObject.toJSONString(queryParams)));
                        }
                        LoggerUtil.info("MyRequestParamsGatewayFilter 入参全局过滤器 ---- end");
                    }
            }));
        } else if (SysKeyEnum.POST.getKey().equalsIgnoreCase(method) && MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
            ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
            // ServerRequest serverRequest = new DefaultServerRequest(exchange);
            // 读取 body 中的内容并修改
            AtomicReference<JSONObject> bodyJsonObj = new AtomicReference<>(new JSONObject());
            Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
                bodyJsonObj.set(JSON.parseObject(body));
                if (SysStateEnum.STATE_YES.getDescribe().equalsIgnoreCase(myRequestParamsGatewayFilterFactoryConfig.getIsDecrypt())) {
                    LoggerUtil.info("解密成功");
                }
                return Mono.just(JSONObject.toJSONString(bodyJsonObj));
            });
            /*
             * TODO 下面的将请求体再次封装写回到request里,传到下一级,否则,由于请求体已被消费,后续的服务将取不到值
             * CacheBodyGlobalFilter这个全局过滤器的目的就是把原有的request请求中的body内容读出来,并且使用ServerHttpRequestDecorator这个请求装饰器对request进行包装,重写getBody方法,并把包装后的请求放到过滤器链中传递下去。
             * 这样后面的过滤器中再使用exchange.getRequest().getBody()来获取body时,实际上就是调用的重载后的getBody方法,获取的最先已经缓存了的body数据
             */
            BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            // the new content type will be computed by bodyInserter and then set in the request decorator
            headers.remove(HttpHeaders.CONTENT_LENGTH);
            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
            return bodyInserter.insert(outputMessage,  new BodyInserterContext()).then(Mono.defer(() -> {
                ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                    @Override
                    public HttpHeaders getHeaders() {
                        long contentLength = headers.getContentLength();
                        HttpHeaders httpHeaders = new HttpHeaders();
                        httpHeaders.putAll(super.getHeaders());
                        if (contentLength > 0) {
                            httpHeaders.setContentLength(contentLength);
                        } else {
                            httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                        }
                        return httpHeaders;
                    }
                    @Override
                    public Flux<DataBuffer> getBody() {
                        Flux<DataBuffer> dataBufferFlux = outputMessage.getBody();
                        return dataBufferFlux;
                    }
                };
                // 从原始交换对象获得一个新的 ServerWebExchange 实例
                ServerWebExchange serverWebExchangeMutable = exchange.mutate().request(serverHttpRequestDecorator).build();
                return chain.filter(serverWebExchangeMutable)
                    // TODO 网关“后置”过滤器逻辑
                    .then(Mono.fromRunnable(() -> {
                        if (SysStateEnum.STATE_YES.getDescribe().equalsIgnoreCase(myRequestParamsGatewayFilterFactoryConfig.getIsPrint())) {
                            LoggerUtil.info("");
                            LoggerUtil.info(MessageFormat.format("MyRequestParamsGatewayFilter ---- start [POST]:{0}", reqPath));
                            if (SysStateEnum.STATE_YES.getDescribe().equalsIgnoreCase(myRequestParamsGatewayFilterFactoryConfig.getIsDecrypt())) {
                                LoggerUtil.info(MessageFormat.format("请求入参解密前: {0}", bodyJsonObj.get().toJSONString()));
                                LoggerUtil.info(MessageFormat.format("请求入参解密后: {0}", bodyJsonObj.get().toJSONString()));
                            } else {
                                LoggerUtil.info(MessageFormat.format("请求入参: {0}", bodyJsonObj.get().toJSONString()));
                            }
                            LoggerUtil.info("MyRequestParamsGatewayFilter ---- end");
                            LoggerUtil.info("");
                        }
                }));
            }));
        } else {
            return chain.filter(exchange);
        }
    }
}
2、创建过滤器参数配置类
/**
 * @描述: 请求参数网关过滤器配置类
 * @作者: lixing
 * @日期 2021/6/25 9:22
 */
@Data
@NoArgsConstructor
public class MyRequestParamsGatewayFilterFactoryConfig {
    /** 是否打印请求参数 */
    private String isPrint;
    /** 是否解密请求参数 */
    private String isDecrypt;
}
3、创建过滤器工厂
/**
 * @描述: TODO 请求参数网关过滤器工厂,需要为相关路由配置上才可生效
 * @作者: lixing
 * @日期 2021/6/24 10:25
 */
@Component
public class MyRequestParamsGatewayFilterFactory extends AbstractGatewayFilterFactory<MyRequestParamsGatewayFilterFactoryConfig> {
    /*
     * 构造函数
     */
    public MyRequestParamsGatewayFilterFactory() {
        super(MyRequestParamsGatewayFilterFactoryConfig.class);
    }

    /*
     * 自定义配置类-内部类,用于传入配置参数
     */
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("isPrint","isDecrypt");
    }

    /*
     * 过滤器逻辑
     */
    @Override
    public GatewayFilter apply(MyRequestParamsGatewayFilterFactoryConfig config) {
        return new MyRequestParamsGatewayFilter(config);
    }
}
4、配置IP网关过滤器
# TODO 基于 【BFF-AUTH】 服务的路由配置
- id: BFF-AUTH-ROUTE
  uri: lb://bffAuth
  predicates:
    - Path=/springCloudGateway/auth/**
  filters:
    # 自定义 [MyIpGatewayFilter] 网关过滤器。*表示允许所有ip请求,其它格式为:127.0.0.1,192.168.0.110
    - MyIp=127.0.0.1,192.168.1.1
    # 自定义 [MyRequestParamsGatewayFilter] 过滤器。参数1:是否打印请求参数,参数2:是否解密请求参数
    - MyRequestParams=Y,Y

文章来源地址https://www.toymoban.com/news/detail-627919.html

到了这里,关于SpringCloudGateway过滤器(全局认证、IP拦截、请求参数过滤、响应参数过滤)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 过滤器和拦截器的辨析

    过滤器和拦截器都是为了在请求到达目标处理器(Servlet或Controller)之前或者之后插入自定义的处理逻辑 过滤器 : 遵循AOP(面向切面编程)思想实现,基于Servlet规范提供的Filter接口,它是位于客户端请求与服务器响应之间的一个组件,依赖于Servlet容器。当请求到达服务器时,过滤器

    2024年03月11日
    浏览(32)
  • 过滤器Filter,拦截器Interceptor

    过滤器Filter 快速入门   详情 登录校验-Filter 拦截器Interceptor 简介快速入门 定义拦截器 配置拦截器 详解(拦截路径,执行流程) 登录校验-Interceptor

    2024年02月07日
    浏览(34)
  • JAVA中的拦截器、过滤器

    相关解释:拦截器依赖于页面有访问controller的操作,且属于SpringMVC体系的动态拦截调用机制,是java中AOP思想的运用。 来看看源码作者的注释: 其中倒数第二段话,描述其类似于过滤器,但其特点只允许使用自定义预处理,不能处理程序本身。此处可体现AOP思想。 过滤器是

    2024年02月13日
    浏览(29)
  • Gateway网关 全局过滤器

    一、全局过滤器 全局过滤器GlobalFilter 全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。 区别在于GatewayFilter通过配置定义,处理逻辑是固定的。 需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件: 参数中是否有au

    2024年02月07日
    浏览(39)
  • 过滤器,监听器与拦截器的区别

    ​ 过滤器和监听器不是Spring MVC中的组件,而是Servlet的组件,由Servlet容器来管理。拦截器是Spring MVC中的组件,由Spring容器来管理 ​ Servlet过滤器与Spring MVC 拦截器在Web应用中所处的层次如下图所示: 过滤器是Servlet的高级特性之一,是实现Filter接口的Java类。其基本功能就是对

    2024年02月14日
    浏览(40)
  • 【SpringBoot】过滤器,监听器,拦截器介绍

    通过两幅图我们可以理解拦截器和过滤器的特点 1、过滤器 过滤器是在请求进入tomcat容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前。 理解上面这句话我们就可以知道,进入servlet之前,主要是两个参数:ServletRequest,

    2024年02月04日
    浏览(45)
  • 【SpringBoot篇】Interceptor拦截器 | 拦截器和过滤器的区别

    拦截器(Interceptor)是一种软件设计模式,用于在应用程序处理请求或响应时对其进行拦截和修改。拦截器可以在整个应用程序中使用,用于执行跨越多个层的通用任务,如身份验证、授权、缓存、日志记录、性能计量等。 在Web开发中,拦截器通常用于在请求到达控制器之前

    2024年02月04日
    浏览(48)
  • Springboot中使用拦截器、过滤器、监听器

    Javaweb三大组件:servlet、Filter(过滤器)、 Listener(监听器) SpringBoot特有组件:Interceptor(拦截器) 过滤器、拦截器、监听器、AOP(后续文章介绍)、全局异常处理器(后续文章介绍)是搭建系统框架时,经常用到的部分,全局异常处理器的作用很明显,就是处理接口执行

    2024年02月03日
    浏览(30)
  • spring boot 过滤器&拦截器与aop

    在使用 Spring 框架时,可以通过在 web.xml 配置文件中注册过滤器,使其在请求进入 Spring 前就能够进行预处理。这样可以在请求进入 Spring MVC 的 DispatcherServlet 之前,对请求进行拦截、修改或者过滤。 过滤器在 Spring 中的应用场景包括但不限于: 字符编码过滤:通过过滤器,在

    2024年02月01日
    浏览(39)
  • Spring Boot拦截器与过滤器的区别

    在使用Spring Boot开发Web应用程序时,您可能需要在处理请求之前或之后执行某些操作。这些操作可以包括身份验证、日志记录、性能监测等。在这种情况下,您可以使用两种不同的机制:拦截器和过滤器。本文将介绍这两种机制及其区别,并提供一些示例代码来演示如何在S

    2024年02月08日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包