Spring Boot 实现接口访问管控的几种方案

Spring Boot 实现接口访问管控的三种方案

方案一:

设计思路

在 Spring Boot 项目中,为了实现暂时禁用某些接口的功能,我们可以采用以下设计思路:

  1. 配置管理:使用配置文件来管理需要禁用的接口列表,这样可以方便地在不修改代码的情况下调整禁用的接口。
  2. 拦截器:创建一个拦截器,在请求到达控制器之前进行拦截,检查请求的接口是否在禁用列表中。如果在禁用列表中,则返回相应的错误信息,阻止请求继续处理。
  3. 配置拦截器:将拦截器注册到 Spring Boot 项目的拦截器链中,使其生效。

具体步骤和代码实现

1. 创建 Spring Boot 项目

首先,你可以使用 Spring Initializr(https://start.spring.io/)创建一个基本的 Spring Boot 项目,添加 Spring Web 依赖。

2. 配置禁用接口列表

在 application.properties 或 application.yml 中添加需要禁用的接口列表。这里以 application.yml 为例:

disabled:
  interfaces:
    - /api/test1
    - /api/test2
3. 创建拦截器

创建一个拦截器类,实现 HandlerInterceptor 接口,在 preHandle 方法中检查请求的接口是否在禁用列表中。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

@Component
public class InterfaceDisableInterceptor implements HandlerInterceptor {

    @Value("#{'${disabled.interfaces}'.split(',')}")
    private List<String> disabledInterfaces;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestUri = request.getRequestURI();
        if (disabledInterfaces.contains(requestUri)) {
            response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
            response.getWriter().write("该接口暂时不可用,请稍后再试。");
            return false;
        }
        return true;
    }
}

代码解释

  • @Value("#{'${disabled.interfaces}'.split(',')}"):从配置文件中读取禁用的接口列表,并将其分割成一个字符串列表。
  • preHandle 方法:在请求处理之前被调用,检查请求的 URI 是否在禁用列表中。如果是,则返回 503 状态码和错误信息,并阻止请求继续处理;否则,允许请求继续处理。
4. 配置拦截器

创建一个配置类,实现 WebMvcConfigurer 接口,将拦截器注册到 Spring Boot 项目的拦截器链中。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private InterfaceDisableInterceptor interfaceDisableInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interfaceDisableInterceptor).addPathPatterns("/**");
    }
}

代码解释

  • @Configuration:表示这是一个配置类。
  • addInterceptors 方法:将 InterfaceDisableInterceptor 拦截器注册到拦截器链中,并设置拦截所有请求。
5. 创建测试接口

创建一个简单的控制器,包含一些测试接口。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/test1")
    public String test1() {
        return "这是测试接口 1";
    }

    @GetMapping("/test2")
    public String test2() {
        return "这是测试接口 2";
    }

    @GetMapping("/test3")
    public String test3() {
        return "这是测试接口 3";
    }
}
测试

启动 Spring Boot 项目,访问 /api/test1 和 /api/test2 接口,会看到返回的错误信息 “该接口暂时不可用,请稍后再试。”;访问 /api/test3 接口,则会正常返回测试信息。

通过以上步骤,我们就实现了在 Spring Boot 项目中临时禁用某些接口的功能。这种方式可以方便地在不修改代码的情况下调整禁用的接口,提高了系统的灵活性和可维护性。

方案二:

使用 AOP(面向切面编程)也可以实现 Spring Boot 项目中临时禁用某些接口的功能。AOP 允许我们在程序的特定点(如方法调用前后)插入额外的逻辑,因此可以利用它来拦截接口请求并根据配置决定是否允许访问。

设计思路

  1. 配置管理:和之前一样,使用配置文件来管理需要禁用的接口列表。
  2. AOP 切面:创建一个切面类,在控制器方法执行前进行拦截,检查请求的接口是否在禁用列表中。如果在禁用列表中,则抛出异常或返回错误信息,阻止方法继续执行。
  3. 定义切入点:明确哪些控制器方法需要被拦截。

具体步骤和代码实现

1. 创建 Spring Boot 项目

使用 Spring Initializr(https://start.spring.io/)创建一个基本的 Spring Boot 项目,添加 Spring Web 和 Spring AOP 依赖。

2. 配置禁用接口列表

在 application.yml 中添加需要禁用的接口列表:

disabled:
  interfaces:
    - /api/test1
    - /api/test2
3. 创建 AOP 切面类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Aspect
@Component
public class InterfaceDisableAspect {

    @Value("#{'${disabled.interfaces}'.split(',')}")
    private List<String> disabledInterfaces;

    @Before("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
    public void checkDisabledInterface(JoinPoint joinPoint) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String requestUri = request.getRequestURI();
            if (disabledInterfaces.contains(requestUri)) {
                throw new InterfaceDisabledException("该接口暂时不可用,请稍后再试。");
            }
        }
    }

    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
    public static class InterfaceDisabledException extends RuntimeException {
        public InterfaceDisabledException(String message) {
            super(message);
        }
    }
}

代码解释

  • @Aspect:表明这是一个切面类。
  • @Before:定义了一个前置通知,在被 @RestController 或 @Controller 注解的类中的方法执行前执行。
  • checkDisabledInterface 方法:获取当前请求的 URI,检查其是否在禁用列表中。如果是,则抛出 InterfaceDisabledException 异常。
  • InterfaceDisabledException:自定义异常类,使用 @ResponseStatus 注解将异常映射为 503 状态码。
4. 创建测试接口
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/test1")
    public String test1() {
        return "这是测试接口 1";
    }

    @GetMapping("/test2")
    public String test2() {
        return "这是测试接口 2";
    }

    @GetMapping("/test3")
    public String test3() {
        return "这是测试接口 3";
    }
}
测试

启动 Spring Boot 项目后,访问 /api/test1 和 /api/test2 接口,会收到 503 状态码和错误信息 “该接口暂时不可用,请稍后再试。”;而访问 /api/test3 接口则会正常返回测试信息。

通过使用 AOP,我们可以方便地在不修改控制器代码的情况下实现接口的临时禁用功能,提高了代码的可维护性和灵活性。

方案三:

一、场景背景

在软件开发进程里,我们经常会碰到要对特定接口实施临时访问管控的情形。比如,当某个接口暴露出安全漏洞亟待修复时,为防止外部程序或用户继续访问该接口,就得迅速将其禁用;又或者在系统进行升级维护期间,部分特定接口可能需要暂时关闭,以规避数据不一致或者系统异常等问题。在 Spring Boot 项目中,我们能够借助 AOP(面向切面编程)搭配自定义注解的手段,灵活达成对接口访问的管控。

二、实现思路

我们的核心思路是打造一个自定义注解,用它来标记需要进行访问管控的接口。接着,通过 AOP 切面在这些接口被调用之前进行拦截,依据配置文件里的规则判定是否允许访问。若不允许访问,就抛出异常并返回对应的错误信息;若允许访问,则继续执行接口方法。

三、具体实现步骤

3.1 创建自定义注解

首先,我们要创建一个名为 AccessControlAnnotation 的自定义注解,用于标记需要进行访问管控的接口。

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

// 该注解可应用于方法上
@Target(ElementType.METHOD)
// 注解在运行时保留
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessControlAnnotation {
    // 注解的属性,用于指定配置文件中对应的属性名
    String configKey(); 
}

3.2 配置异常类与结果码

为了在接口被禁用时能够返回统一的错误信息,我们需要定义一个自定义异常类 AccessDeniedException 和结果码枚举 ResponseCode

// ResponseCode.java
public enum ResponseCode {
    // 定义接口未找到的结果码及对应信息
    INTERFACE_NOT_FOUND(404, "接口未找到"); 

    private final int code;
    private final String message;

    // 枚举的构造函数
    ResponseCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    // 获取结果码
    public int getCode() {
        return code;
    }

    // 获取结果码对应的信息
    public String getMessage() {
        return message;
    }
}

// AccessDeniedException.java
public class AccessDeniedException extends RuntimeException {
    private final int code;

    // 异常类的构造函数
    public AccessDeniedException(int code, String message) {
        super(message);
        this.code = code;
    }

    // 获取异常对应的结果码
    public int getCode() {
        return code;
    }
}

3.3 创建 AOP 切面类

接下来,我们创建一个名为 InterfaceAccessControlAspect 的 AOP 切面类,用于拦截带有 AccessControlAnnotation 注解的接口方法。

import cn.hutool.core.text.CharSequenceUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

// 声明这是一个切面类
@Aspect
// 将该类注册为 Spring 组件
@Component
// 设置切面的执行顺序
@Order(-1)
public class InterfaceAccessControlAspect {

    // 注入 Spring 的 Environment 对象,用于获取配置信息
    @Autowired
    @Lazy
    private Environment environment;

    /**
     * 定义切入点,拦截所有带有 AccessControlAnnotation 注解的 Controller 接口方法。
     */
    @Pointcut("bean(*Controller) " +
            "&& @annotation(com.example.annotation.AccessControlAnnotation)")
    public void accessControlPointCut() {
        // 空方法体,仅用于定义切入点
    }

    /**
     * 环绕通知,在目标方法执行前后进行处理。
     */
    @Around("accessControlPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 标记接口是否可访问,初始为可访问
        boolean isAccessible = true;
        // 获取目标对象的类
        Class<?> targetClass = joinPoint.getTarget().getClass();
        // 获取方法签名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 获取目标方法
        Method targetMethod = targetClass.getDeclaredMethod(methodSignature.getName(), methodSignature.getParameterTypes());
        // 获取方法上的 AccessControlAnnotation 注解
        AccessControlAnnotation accessAnnotation = targetMethod.getAnnotation(AccessControlAnnotation.class);
        // 获取注解中指定的配置文件属性名
        String configPropertyName = accessAnnotation.configKey();
        // 获取当前请求的 HttpServletRequest 对象
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        // 获取请求的 URI
        String requestUri = request.getRequestURI();
        // 获取请求的方法(如 GET、POST 等)
        String requestMethod = request.getMethod();

        // 如果配置属性名不为空
        if (CharSequenceUtil.isNotBlank(configPropertyName)) {
            // 用于存储禁用的接口 URI 的集合
            Set<String> disabledInterfaceSet = new HashSet<>();
            // 循环读取配置文件中以配置属性名开头的数组配置
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                // 获取配置文件中的属性值
                String propertyValue = environment.getProperty(configPropertyName + "[" + i + "]");
                // 如果属性值为空,跳出循环
                if (propertyValue == null) {
                    break;
                }
                // 如果属性值不为空,将其添加到禁用接口集合中
                if (StringUtils.isNotBlank(propertyValue)) {
                    disabledInterfaceSet.add(propertyValue);
                }
            }
            // 判断请求的 URI 是否在禁用接口集合中,如果在则不可访问
            isAccessible = !disabledInterfaceSet.contains(requestUri);
        }

        // 如果接口可访问,继续执行目标方法
        if (isAccessible) {
            return joinPoint.proceed();
        } else {
            // 如果接口不可访问,抛出 AccessDeniedException 异常
            throw new AccessDeniedException(ResponseCode.INTERFACE_NOT_FOUND.getCode(), String.format("未找到处理 %s %s 的处理器", requestMethod.toUpperCase(), requestUri));
        }
    }
}

3.4 配置文件

在 application.properties 或 application.yml 中配置需要禁用的接口列表。以下是 application.properties 的示例:

# 定义禁用接口的配置属性,使用数组形式
disabled.interfaces[0]=/api/test1
disabled.interfaces[1]=/api/test2

3.5 创建测试接口

创建一个简单的控制器,包含一些测试接口,并使用 AccessControlAnnotation 注解标记需要进行访问管控的接口。

import com.example.annotation.AccessControlAnnotation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// 声明这是一个 RESTful 风格的控制器
@RestController
// 定义请求映射的基础路径
@RequestMapping("/api")
public class TestController {

    /**
     * 测试接口 1,使用 AccessControlAnnotation 注解标记,指定配置属性名
     */
    @GetMapping("/test1")
    @AccessControlAnnotation(configKey = "disabled.interfaces")
    public String test1() {
        return "这是测试接口 1";
    }

    /**
     * 测试接口 2,使用 AccessControlAnnotation 注解标记,指定配置属性名
     */
    @GetMapping("/test2")
    @AccessControlAnnotation(configKey = "disabled.interfaces")
    public String test2() {
        return "这是测试接口 2";
    }

    /**
     * 测试接口 3,未使用 AccessControlAnnotation 注解标记,不进行访问管控
     */
    @GetMapping("/test3")
    public String test3() {
        return "这是测试接口 3";
    }
}

四、测试与验证

启动 Spring Boot 项目之后,尝试访问 /api/test1 和 /api/test2 接口。由于这两个接口被配置为禁用,你会收到 404 状态码以及错误信息 "未找到处理 GET /api/test1 的处理器" 或者 "未找到处理 GET /api/test2 的处理器";而访问 /api/test3 接口时,会正常返回测试信息,因为该接口并未使用 AccessControlAnnotation 注解进行标记。

五、总结

借助自定义注解和 AOP 切面的方式,我们能够灵活地对 Spring Boot 项目中的接口实施访问管控。这种方式不但提升了代码的可维护性与可扩展性,还能依据不同的业务需求便捷地调整接口访问规则。同时,运用自定义异常类和结果码能够统一错误处理,为用户提供清晰明了的错误信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值