好记性不如烂笔头,趁热记录下,给未来的自己。

0 | 前言

在 《事务开发时,接口不能对外露出怎么办?》 一文中,咱们介绍了经过网关 + AOP 的计划,来制止指定接口的对外露出。

这种办法会在任何布置环境下,都制止该接口的外部拜访。

但是,咱们知道,比较正规的项目开发,都会有开发,测验,(预生产),生产等布置环境。一般,在开发环境下,研发同学在布置到环境之后,需求在本地调一下接口,来做必定的接口自测。如果这个接口只允许集群内网拜访,本地调用就会比较麻烦:

  1. 要么把本地环境接入到集群内网(《微服务后端开发的痛点,你是否曾经历过?》);
  2. 要么在布置开发环境的时候,把对应接口的内网拜访限制封闭,等上其他环境再打开

这两种办法都有必定的工作量,那么有没有一种计划,在装备接口是否只能内网拜访的时候,同时装备某个/些环境,允许外网拜访?

本文,将针对这个需求,来提供完成计划。点个赞再看:>

1 | 计划 Preview

1.1 | 计划条件

本计划依据 java + springboot 这套技能栈,会涉及到的技能点有:

  1. AOP 切面编程
  2. Annotation 注解

1.2 | 核心逻辑

核心逻辑仍是遵循 《事务开发时,接口不能对外露出怎么办?》:

  1. 会在外网进来的使用网关(没有使用网关的,可用流量网关如nginx) 处,增加一个 header,用于给外部流量打一个标签。
  2. 然后在事务代码的 controller 层,增加一个注解,经过 AOP 的办法来判断是否有外部标签,
  3. 同时会依据注解传入的环境变量信息和当时地点的环境,综合判断是否放行该请求。

根据部署的环境,来决定接口是否可以对外暴露 | 业务开发时,接口不能对外暴露怎么办 - 第二弹

2 | 详细完成

2.1 | Annotation 注解代码

被注解的接口只允许内网拜访,不允许经过网关的外网拜访, exceptEnvs={} 声明的环境列表在外

/**
 * 被注解的接口只允许内网拜访,不允许经过网关的外网拜访, exceptEnvs={} 声明的环境列表在外
 *
 * @author lanbitou
 * @date 2021/12/17
 * @project common-service
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OnlyIntranetAccessV2 {
    DeployEnvEnum[] exceptEnvs() default {};
}

2.2 | AOP 切面代码

接口只允许内网调用,来自 Gateway 的请求( header 里存在 k-v = {"from": "gateway"}),会被 denied 掉, 除非注解传入的 exceptEnvs${spring.profiles.active} 共同(即期望被扫除的环境和代码当时运行的环境共同)。

需求注意的是,加了传参的注解,只能 annotate 到办法上,无法到类。 这一点,在下面的代码里有所体现。

/**
 * 接口只允许内网调用,来自网关的请求,会被denied掉
 *
 * @author lanbitou
 * @date 2021/12/7
 * @project common-service
 */
@Aspect
@Component
@Slf4j
@Order(-96)
public class OnlyIntranetAccessAspectV2 {
    @Value("${spring.profiles.active}")
    private String crtEnv;
    @Pointcut("@within(org.lanbitou.common.annotation.OnlyIntranetAccess)")
    public void onlyIntranetAccessOnClass() {}
    @Pointcut("@annotation(org.lanbitou.common.annotation.OnlyIntranetAccess)&&@annotation(onlyIntranetAccessV2)")
    public void onlyIntranetAccessOnMethed(OnlyIntranetAccessV2 onlyIntranetAccessV2) {
    }
    @Before(value = "onlyIntranetAccessOnMethed(onlyIntranetAccessV2)", argNames = "onlyIntranetAccessV2")
    public void before1(OnlyIntranetAccessV2 onlyIntranetAccessV2) {
        if (onlyIntranetAccessV2.exceptEnvs().length != 0) {
            crtEnv = crtEnv == null ? DeployEnvEnum.ENV_PROD.getName() : crtEnv;
            for (DeployEnvEnum exceptEnv : onlyIntranetAccessV2.exceptEnvs()) {
                if (crtEnv.contains(exceptEnv.getName())) {
                    return;
                }
            }
        }
        doCheck();
    }
    @Before(value = "onlyIntranetAccessOnClass()")
    public void before2() {
        doCheck();
    }
    private void doCheck() {
        HttpServletRequest hsr = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String from = hsr.getHeader("from");
        if (!StringUtils.isEmpty(from) && "gateway".equals(from)) {
            log.error("This api is only allowed invoked by intranet source");
            throw new MMException(ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
        }
    }
}

2.3 | Controller 层使用

Controller 层在需求的 API 上增加 @OnlyIntranetAccessV2(exceptEnvs = {DeployEnvEnum.ENV_DEV})。其中 DeployEnvEnum.ENV_DEV 是指定需求扫除的环境。多个环境,能够逗号隔开。

@OnlyIntranetAccessV2(exceptEnvs = {DeployEnvEnum.ENV_DEV})
public ResponseEntity<ReturnBase> controllerApi(@Validated @RequestBody CheckitAuditReqDto reqDto) {
    return Optional.ofNullable(auditService.fileAuditAsync(reqDto))
            .map(ret -> new ResponseEntity<>(ret, HttpStatus.OK))
            .orElseThrow(() -> new MMException("error.audit.file", ReturnEnum.C_GENERAL_BUSINESS_ERROR.getMsgCode()));
}

这样,当开发同学对只能内网拜访的接口暂时在开发环境上经过外网拜访的话,就能够经过这个计划来操作了。

需求注意的是,任何没有安全措施的接口露出都可能带来意想不到的后果,即使是开发环境。

以上。