小白学习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

相关推荐
Wx-bishekaifayuan11 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer0811 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Stringzhua12 小时前
【SpringCloud】Kafka消息中间件
spring·spring cloud·kafka
想进大厂的小王18 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
customer0818 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
杨荧19 小时前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
aloha_7891 天前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
茶馆大橘1 天前
微服务系列五:避免雪崩问题的限流、隔离、熔断措施
java·jmeter·spring cloud·微服务·云原生·架构·sentinel
荆州克莱2 天前
[FE] React 初窥门径(四):React 组件的加载过程(render 阶段)
spring boot·spring·spring cloud·css3·技术