使用SpringCloud架构中在不作任何处理的情况下我们是可以绕过网关直接访问后端服务的。因此会带来很大的安全隐患.

解决方案

1.网络隔离
将后端普通服务都部署在内网,通过防火墙策略限制只允许网关应用访问后端服务。

2.应用层拦截
请求后端服务时通过拦截器校验请求是否来自网关和服务间的调用,如果是非法调用则提示不允许访问。

在请求经过网关的时候给请求头中增加一个额外的Header表示请求来源,在后端服务中写一个拦截器,判断请求头是否是来自网关或者是内部服务调用,如果不一致则不允许访问并给出提示。

当然为了防止在每个后端服务都需要编写这个拦截器,我们可以将其写在一个公共的starter中,让后端服务引用即可。而且为了灵活,可以通过配置决定是否只允许后端服务访问。

2.1 在网关gateway模块编写网关过滤器

package cn.myxl.gateway.filter;

import cn.myxl.gateway.constant.SecurityConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequest.Builder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* @author alan.yang.zhang
* @date 2023-09-07 22:48
*/

@Component
@Slf4j
public class RequestSourceFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest.Builder mutate = exchange.getRequest().mutate();

handleAddHeader(mutate, SecurityConstants.SOURCE, SecurityConstants.SOURCE_GATEWAY);

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

return chain.filter(webExchange);
}

private void handleAddHeader(Builder mutate, String headerName, String headerValue) {
mutate.header(headerName, headerValue);
}

private void handleRemoveHeader(Builder mutate, String headerKey) {
mutate.headers(httpHeaders -> httpHeaders.remove(headerKey)).build();
}

@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 100;
}

2.1 创建Starter模块service-security-spring-boot-starter

  • 创建annotation SecurityCheck 用来标记需要检查的方法
package cn.myxl.cloud.security.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author alan.yang.zhang
* @date 2023-09-08 02:43
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SecurityCheck {
}
  • ServiceSecurityAutoConfiguration.java 是自动配置类,负责将自定义 Starter 中的服务注入到 Spring 容器中;
  • ServiceSecurityProperties.java 是自定义 Starter 中的配置属性;
  • spring.factories 是 Spring Boot 自动装配的核心配置文件,用于声明自动配置类。
  • 创建starter配置类
package cn.myxl.cloud.security.starter;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* @author alan.yang.zhang
* @date 2023-09-08 02:43
*/
@Data
@ConfigurationProperties(prefix = "service.security")
public class ServiceSecurityProperties {
private Boolean enabled;
}
  • 创建starter自动配置类
package cn.myxl.cloud.security.starter;

import cn.myxl.cloud.security.aspect.SecurityCheckAspect;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author alan.yang.zhang
* @date 2023-09-08 04:04
*/

@Configuration
@EnableConfigurationProperties({ServiceSecurityProperties.class})
@ConditionalOnProperty(prefix = "service.security", value = "enabled")
public class ServiceSecurityAutoConfiguration {
@Bean
public SecurityCheckAspect securityCheckAspect() {
return new SecurityCheckAspect();
}
}
  • 创建spring.factories 注意文件位置(src/main/resources/META-INF/spring.factories)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.myxl.cloud.security.starter.ServiceSecurityAutoConfiguration
  • 业务切面逻辑,用于校验请求是否经过网关
package cn.myxl.cloud.security.aspect;

import cn.myxl.cloud.security.annotation.SecurityCheck;
import cn.myxl.cloud.security.constant.SecurityConstants;
import cn.myxl.cloud.security.exception.SecurityCheckException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
* @author alan.yang.zhang
* @date 2023-09-08 03:01
*/
@Aspect
@Component
@Slf4j
public class SecurityCheckAspect implements Ordered {

@Before("@annotation(securityCheck)")
public void doSecurityCheckBefore(JoinPoint point, SecurityCheck securityCheck) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

String source = attributes.getRequest().getHeader(SecurityConstants.SOURCE);

log.debug("[SecurityCheckAspect] [doSecurityCheckBefore] source 值为", source);

if (!SecurityConstants.SOURCE_GATEWAY.equals(source) && !SecurityConstants.SOURCE_SERVICE_CALL.equals(source)) {
log.debug("[SecurityCheckAspect] [doSecurityCheckBefore] 没有内部访问权限,不允许访问");

throw new SecurityCheckException("没有内部访问权限,不允许访问");
}
}

@Override
public int getOrder() {
return 0;
}
}
  • 编写constant工具类
package cn.myxl.cloud.security.constant;

/**
* @author alan.yang.zhang
* @date 2023-09-08 03:19
*/
public class SecurityConstants {
public static final String SOURCE = "X-Request-Source";

public static final String SOURCE_GATEWAY = "source-gateway";

public static final String SOURCE_SERVICE_CALL = "source-service-call";

}
  • 编写SecurityCheckException工具类
package cn.myxl.cloud.security.exception;

/**
* @author alan.yang.zhang
* @date 2023-09-08 03:53
*/

public class SecurityCheckException extends RuntimeException {

private static final long serialVersionUID = 2984861906864505926L;

/**
* Constructs a new runtime exception with the specified detail message.
* The cause is not initialized, and may subsequently be initialized by a
* call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public SecurityCheckException(String message) {
super(message);
}
}

经过以上几步,一个公共的Starter模块就构建完成了。 后端服务引用此公共Starter模块即可

<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>service-security-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>

添加注解SecurityCheck 到需要拦截的方法上

@SecurityCheck
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}

需要开启该功能

service:
security:
enabled: true

网关拦截调用完成了

内部服务调用不需要拦截, 所以需要在方法调用处添加 header 标记来源为方法内部调用

@FeignClient(contextId = "userClient", value = ServiceNameConstants.SERVICE_USER)
public interface UserClient {

@GetMapping(value = "/user/{id}", headers = {SecurityConstants.HEADER_SOURCE_SERVICE_CALL})
User findById(@PathVariable("id") Long id);
}

服务间内部调用完成了