业务开发时,接口不能对外暴露解决方案

在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。

1. 内外网接口微服务隔离

将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。
该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。
该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。

2. 网关 + redis 实现白名单机制

在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。
该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;
不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。

3. 方案三 网关 + AOP

相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。
我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。
根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。
该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。
当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。

注解类:

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OnlyIntranetAccess {

}

切面类:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;


@Aspect
@Component
public class OnlyIntranetAccessAspect {

    @Pointcut ( "@within(com.xjw.annotation.OnlyIntranetAccess)" )
    public void onlyIntranetAccessOnClass () {}
    @Pointcut( "@annotation(com.xjw.annotation.OnlyIntranetAccess)" )
    public void onlyIntranetAccessOnMethod () {
    }

    @Before(value = "onlyIntranetAccessOnClass() || onlyIntranetAccessOnMethod()")
    public void before() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String from = (String) request.getAttribute("from");
        if (!StringUtils.isEmpty(from) && from.equals("public")) {
            System.out.println("内部接口,外部无权访问");
            throw new RuntimeException("内部接口,外部无权访问");
        }
    }

}

网管配置类:

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(
                exchange.mutate().request(
                                exchange.getRequest().mutate().header("id", "").header("from", "public").build())
                        .build()
        );
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

测试接口:

import com.xjw.annotation.OnlyIntranetAccess;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/demo")
public class DemoController {

    @OnlyIntranetAccess
    @GetMapping("/1")
    public String demo1() {
        return "内部接口";
    }

    @GetMapping("/2")
    public String demo2() {
        return "非内部接口";
    }

}
posted @ 2023-09-07 10:42  道之缘  阅读(15)  评论(0编辑  收藏  举报