根据部署的环境,来决定接口是否可以对外暴露 | 业务开发时,接口不能对外暴露怎么办 - 第二弹
好记性不如烂笔头,趁热记录下,给未来的自己。
0 | 前言
在 《业务开发时,接口不能对外暴露怎么办?》 一文中,我们介绍了通过 网关 + AOP 的方案,来禁止指定接口的对外暴露。
这种方式会在任何部署环境下,都禁止该接口的外部访问。
然而,我们知道,比较正规的项目开发,都会有开发,测试,(预生产),生产等部署环境。通常,在开发环境下,研发同学在部署到环境之后,需要在本地调一下接口,来做一定的接口自测。如果这个接口只允许集群内网访问,本地调用就会比较麻烦:
- 要么把本地环境接入到集群内网(《微服务后端开发的痛点,你是否曾经历过?》);
- 要么在部署开发环境的时候,把对应接口的内网访问限制关闭,等上其他环境再打开
这两种方式都有一定的工作量,那么有没有一种方案,在配置接口是否只能内网访问的时候,同时配置某个/些环境,允许外网访问?
本文,将针对这个需求,来提供实现方案。点个赞再看:>
1 | 方案 Preview
1.1 | 方案前提
本方案基于 java + springboot 这套技术栈,会涉及到的技术点有:
- AOP 切面编程
- Annotation 注解
1.2 | 核心逻辑
核心逻辑还是遵循 《业务开发时,接口不能对外暴露怎么办?》:
- 会在外网进来的应用网关(没有应用网关的,可用流量网关如nginx) 处,添加一个 header,用于给外部流量打一个标签。
- 然后在业务代码的 controller 层,添加一个注解,通过 AOP 的方式来判断是否有外部标签,
- 同时会根据注解传入的环境变量信息和当前所在的环境,综合判断是否放行该请求。
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()));
}
这样,当开发同学对只能内网访问的接口临时在开发环境上通过外网访问的话,就可以通过这个方案来操作了。
需要注意的是,任何没有安全措施的接口暴露都可能带来意想不到的后果,即使是开发环境。
以上。
转载自:https://juejin.cn/post/7223373957088264229