前言
在开发过程中,网关是一个很重要的角色,在网关中可以添加各种过滤器,过滤请求,保证请求参数安全,限流等等。如果请求绕过了网关,那就等于绕过了重重关卡,直捣黄龙
在分布式架构的系统中,每个服务都有自己的一套API提供给别的服务调用,如何保证每个服务相互之间安全调用?
解决方案
我觉得防止绕过网关直接请求后端服务的解决方案主要有三种:
- 使用
Kubernetes
部署 在使用Kubernetes
部署SpringCloud
架构时我们给网关的Service
配置NodePort
,其他后端服务的Service
使用ClusterIp
,这样在集群外就只能访问到网关了。 - 网络隔离 后端普通服务都部署在内网,通过防火墙策略限制只允许网关应用访问后端服务。
- 应用层拦截 请求后端服务时通过拦截器校验请求是否来自网关,如果不来自网关则提示不允许访问。
这里我们着重关注在应用层拦截这种解决方案。
思路
- 当请求经过网关时,通过全局过滤器(Filter),在请求头添加一个经过网关的密钥。
- 在服务中添加过滤器,验证密钥,如果密钥存在,并且正确,则表示请求来自网关,否则不是,并禁止通过。
本文介绍如何限制请求绕过网关,直接访问服务。为了防止在每个后端服务都需要编写这个拦截器,我们可以将其写在一个公共的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