小白学习spring-cloud(十七):禁止直接访问服务

前言

在开发过程中,网关是一个很重要的角色,在网关中可以添加各种过滤器,过滤请求,保证请求参数安全,限流等等。如果请求绕过了网关,那就等于绕过了重重关卡,直捣黄龙

在分布式架构的系统中,每个服务都有自己的一套API提供给别的服务调用,如何保证每个服务相互之间安全调用?

解决方案

我觉得防止绕过网关直接请求后端服务的解决方案主要有三种:

  • 使用Kubernetes部署 在使用Kubernetes部署SpringCloud架构时我们给网关的Service配置NodePort,其他后端服务的Service使用ClusterIp,这样在集群外就只能访问到网关了。
  • 网络隔离 后端普通服务都部署在内网,通过防火墙策略限制只允许网关应用访问后端服务。
  • 应用层拦截 请求后端服务时通过拦截器校验请求是否来自网关,如果不来自网关则提示不允许访问。

这里我们着重关注在应用层拦截这种解决方案。

思路

  1. 当请求经过网关时,通过全局过滤器(Filter),在请求头添加一个经过网关的密钥。
  2. 在服务中添加过滤器,验证密钥,如果密钥存在,并且正确,则表示请求来自网关,否则不是,并禁止通过。

本文介绍如何限制请求绕过网关,直接访问服务。为了防止在每个后端服务都需要编写这个拦截器,我们可以将其写在一个公共的starter中,让后端服务引用即可。而且为了灵活,可以通过配置决定是否只允许后端服务访问。

实现过程

  • 在网关gateway-global-filter模块编写网关过滤器。
java 复制代码
package com.gateway.filter;

import com.example.constant.CloudConstant;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@Order(0)
public class GatewayRequestGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        byte[] token = Base64Utils.encode((CloudConstant.GATEWAY_TOKEN_VALUE).getBytes());
        String[] headerValues = {new String(token)};
        ServerHttpRequest build = exchange.getRequest()
                .mutate()
                .header(CloudConstant.GATEWAY_TOKEN_HEADER, headerValues)
                .build();

        ServerWebExchange newExchange = exchange.mutate().request(build).build();

        return chain.filter(newExchange);
    }

}
  • CloudConstant如下:
java 复制代码
package com.example.constant;

public class CloudConstant {
    public static final String GATEWAY_TOKEN_HEADER = "gatewayTokenHeader";

    public static final String GATEWAY_TOKEN_VALUE = "42cbf4eb64cc444e";
}

在请求经过网关时添加额外的Header,为了方便这里直接设置成固定值。

  • 建立公共Starter模块cloud-component-security-starter 我这里使用springboot项目,单独建了一个starter项目,并将其上传到了github仓库,可在任何项目中以第三方依赖的形式引入。
  • pom文件,内含将starter部署到github的配置
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.6.12</version>
       <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.gateway.security</groupId>
    <artifactId>cloud-component-security-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cloud-component-security-starter</name>
    <description>cloud-component-security-starter</description>

    <properties>
       <java.version>8</java.version>
       <!-- 此处配置的名称要和maven配置文件对应的serverId一致 -->
       <github.global.server>github</github.global.server>
    </properties>

    <dependencies>
       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <dependency>
          <groupId>org.apache.commons</groupId>
          <artifactId>commons-lang3</artifactId>
          <version>3.12.0</version>
       </dependency>

       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-autoconfigure</artifactId>
       </dependency>

       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-configuration-processor</artifactId>
          <optional>true</optional>
       </dependency>

       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
       </dependency>

       <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
       </dependency>

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

       <dependency>
          <groupId>cn.hutool</groupId>
          <artifactId>hutool-all</artifactId>
          <version>5.8.15</version>
       </dependency>
    </dependencies>

    <build>
       <!-- 此tag下面的所有plugins都是关于上传jar包的依赖 -->
       <plugins>
          <plugin>
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-jar-plugin</artifactId>
             <version>3.0.2</version>
          </plugin>

          <plugin>
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-compiler-plugin</artifactId>
             <version>3.5.1</version>
          </plugin>

          <plugin>
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-source-plugin</artifactId>
             <version>3.0.1</version>
             <executions>
                <execution>
                   <phase>package</phase>
                   <goals>
                      <goal>jar</goal>
                   </goals>
                </execution>
             </executions>
          </plugin>

          <plugin>
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-deploy-plugin</artifactId>
             <version>2.8.2</version>
             <configuration>
                <!-- 配置本地打包后的本地仓库存储地址,后续上传jar包会从此仓库中去取 -->
                <altDeploymentRepository>
                   internal.repo::default::file://${project.build.directory}/maven-repository
                </altDeploymentRepository>
             </configuration>
          </plugin>

          <plugin>
             <groupId>com.github.github</groupId>
             <artifactId>site-maven-plugin</artifactId>
             <version>0.12</version>
             <configuration>
                <message>Maven artifacts for ${project.artifactId}-${project.version}</message>
                <noJekyll>true</noJekyll>
                <!-- 指定从哪里去取打好的包,并上传至github -->
                <outputDirectory>${project.build.directory}/maven-repository</outputDirectory>
                <!--
                   指定要上传的分支, refs/heads 这个不变,后面的分支名可选,可以采取一个jar包使用一个分支的策略。
                   若多个jar包同时发布在同一个分支的话,会覆盖。。。。
                -->
                <branch>refs/heads/dependency</branch>
                <!-- 包含outputDirectory标签内填的文件夹中的所有内容 -->
                <includes>
                   <include>**/*</include>
                </includes>
                <!-- github远程存储outputDirectory标签内填的文件夹中的内容 -->
                <repositoryName>maven-repository</repositoryName>
                <!--
                  github的用户名,注意不是登录的用户名,此项需要登录后,进入https://github.com/settings/profile页面配置Name属性,
                  否则会报
                  [ERROR] Failed to execute goal com.github.github:site-maven-plugin:0.12:site
                  (default) on project rfcore: Error creating commit: Invalid request.
                  [ERROR] For 'properties/name', nil is not a string.
                  [ERROR] For 'properties/name', nil is not a string. (422)
                  [ERROR] -> [Help 1]
                  的错误
                -->
                <repositoryOwner>hehepeng</repositoryOwner>
             </configuration>
             <executions>
                <execution>
                   <goals>
                      <goal>site</goal>
                   </goals>
                   <phase>deploy</phase>
                </execution>
             </executions>
          </plugin>
       </plugins>
    </build>
</project>
  • 编写配置类,用于灵活控制服务是否允许绕过网关,以及相关参数配置
java 复制代码
package com.gateway.security.properties;

import com.gateway.security.constants.CloudConstant;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = CloudConstant.GATEWAY_TOKEN_PREFIX)
public class CloudSecurityProperties {

    /**
     * 是否只能通过网关获取资源
     * 默认为True
     */
    private Boolean onlyFetchByGateway = Boolean.TRUE;

    /**
     * 判断当前请求是否来自gateway的请求头的key
     */
    private String gatewayTokenHeader = CloudConstant.GATEWAY_TOKEN_HEADER;

    /**
     * 判断当前请求是否来自gateway的请求头的value
     * 如果值一致,则来自gateway
     */
    private String gatewayTokenValue = CloudConstant.GATEWAY_TOKEN_VALUE;

    /**
     * 网关禁止访问的错误响应码
     */
    private int gatewayErrorCode = CloudConstant.GATEWAY_ERROR_CODE;

    /**
     * 网关禁止访问的错误状态码
     */
    private int gatewayErrorStatus = CloudConstant.GATEWAY_ERROR_STATUS;

    /**
     * 网关禁止访问的错误消息
     */
    private String gatewayErrorMessage = CloudConstant.GATEWAY_ERROR_MESSAGE;
}

CloudConstant类:

java 复制代码
package com.gateway.security.constants;

public class CloudConstant {
    public static final String GATEWAY_TOKEN_HEADER = "gatewayTokenHeader";

    public static final String GATEWAY_TOKEN_VALUE = "42cbf4eb64cc444e";

    public static final String GATEWAY_TOKEN_PREFIX = "cloud.security";

    public static final String GATEWAY_ERROR_MESSAGE = "Bad Gateway";

    public static int GATEWAY_ERROR_CODE = 403;

    public static int GATEWAY_ERROR_STATUS = 403;
}
  • 编写拦截器,用于校验请求是否经过网关
java 复制代码
package com.gateway.security.interceptor;

import cn.hutool.json.JSONUtil;
import com.gateway.security.properties.CloudSecurityProperties;
import lombok.NonNull;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Base64Utils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Setter
public class ServerProtectInterceptor implements HandlerInterceptor {

    private CloudSecurityProperties properties;

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws IOException {

        if (!properties.getOnlyFetchByGateway()) {
            return true;
        }

        String token = request.getHeader(properties.getGatewayTokenHeader());

        String gatewayToken = new String(Base64Utils.encode(properties.getGatewayTokenValue().getBytes()));

        if (StringUtils.equals(gatewayToken, token)) {
            return true;
        } else {
            Map<String, Object> resultMap = new HashMap<>(4);
            resultMap.put("success", false);
            resultMap.put("code", properties.getGatewayErrorCode());
            resultMap.put("message", properties.getGatewayErrorMessage());
            resultMap.put("timestamp", new Date().getTime());

            response.setStatus(properties.getGatewayErrorStatus());
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().print(JSONUtil.parse(resultMap));
            response.flushBuffer();
            return false;
        }
    }

}
  • 配置拦截器
java 复制代码
package com.gateway.security.config;

import com.gateway.security.interceptor.ServerProtectInterceptor;
import com.gateway.security.properties.CloudSecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class CloudSecurityInterceptorConfigure implements WebMvcConfigurer {

    private CloudSecurityProperties properties;

    @Autowired
    public void setProperties(CloudSecurityProperties properties) {
        this.properties = properties;
    }

    @Bean
    public HandlerInterceptor serverProtectInterceptor() {
        ServerProtectInterceptor interceptor = new ServerProtectInterceptor();
        interceptor.setProperties(properties);
        return interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(serverProtectInterceptor());
    }
}
  • 编写starter装载类
java 复制代码
package com.gateway.security.config;

import com.gateway.security.properties.CloudSecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@EnableConfigurationProperties(CloudSecurityProperties.class)
public class CloudSecurityAutoConfigure{

    @Bean
    public CloudSecurityInterceptorConfigure cloudSecurityInterceptorConfigure() {
        return new CloudSecurityInterceptorConfigure();
    }

}
  • 建立资源文件spring.factories,配置Bean的自动加载
factories 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.gateway.security.config.CloudSecurityAutoConfigure

使用starter

在要禁止直接访问的后端服务中引入该starter,如nacos-provider服务,并在配置文件中添加属性配置,默认只能通过网关访问

  • 引入cloud-component-security-starter
xml 复制代码
<dependency>
    <groupId>com.gateway.security</groupId>
    <artifactId>cloud-component-security-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
  • application.yml配置,默认为true
yml 复制代码
cloud:
  security:
    only-fetch-by-gateway: true

输入可有如下提示:

实现效果

直接访问后端服务接口,http://localhost:9001/nacos/test

相关推荐
南朝雨4 小时前
Spring Boot Admin日志监控坑点:远程配置的logging.file.name为何生效又失效?
java·spring boot·spring cloud·微服务·logback
深入技术了解原理5 小时前
eureka-client依赖爆红无法下载
spring cloud·云原生·eureka
ZePingPingZe5 小时前
深入理解网络模型之Spring Cloud微服务通信、Socket、HTTP与RPC
网络协议·spring cloud·rpc·dubbo
叫码农就行6 小时前
spring cloud 笔记
java·笔记·spring cloud
七夜zippoe6 小时前
微服务架构演进实战 从单体到微服务的拆分原则与DDD入门
java·spring cloud·微服务·架构·ddd·绞杀者策略
Leo July1 天前
【Java】Spring Cloud 微服务生态全解析与企业级架构实战
java·spring cloud
李慕婉学姐1 天前
【开题答辩过程】以《基于springcloud的空气质量监控管理系统》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
后端·spring·spring cloud
CV_J2 天前
安装kibana
java·elasticsearch·spring cloud·docker·容器
小马爱打代码2 天前
实时搜索:SpringCloud + Elasticsearch + Redis + Kafka
redis·elasticsearch·spring cloud