Spring Boot 接口安全设计:接口限流、防重放攻击、签名验证

在当今互联网应用开发中,接口安全至关重要。对于Spring Boot项目而言,保障接口不被恶意调用、数据不被篡改、请求不被重放,是后端开发者必须攻克的安全难题。

接口限流

为什么需要接口限流

在高并发场景下,接口可能面临大量请求的冲击。如果不加以控制,可能导致服务器资源耗尽,服务响应变慢甚至崩溃。接口限流的主要目的包括:

1、保护后端服务:防止某个接口被恶意请求或突发流量击垮,确保后端服务的稳定性。

2、防止滥用:限制单个用户或客户端对接口的访问频率,避免恶意刷接口行为。

3、节省资源:合理控制流量,保护后端数据库、缓存等资源,提高系统整体性能。

限流算法

常见的限流算法有以下几种:

1、令牌桶算法(Token Bucket):系统按固定速率生成令牌放入桶中,桶有固定容量。客户端请求时需要从桶中获取令牌,若桶中有足够令牌则请求通过,否则请求被拒绝。例如,每秒生成10个令牌,桶容量为100,意味着系统允许一定程度的突发流量,但长期平均下来每秒处理10个请求。

2、漏桶算法(Leaky Bucket):请求像水流一样进入一个固定容量的桶中,桶以固定速率处理请求(漏水),超出桶容量的请求将被丢弃。该算法能保证请求以固定速率被处理,但无法应对突发流量。

3、滑动窗口计数器法(Sliding Window Counter):将时间划分为多个固定大小的窗口,每个窗口记录请求数量。随着时间推移,窗口滑动,通过统计滑动窗口内的请求总数来判断是否限流。与简单的固定窗口计数器法相比,滑动窗口法能更细粒度地控制流量,避免在窗口切换时出现流量突增导致的限流失效问题。

实现接口限流示例

java 复制代码
public class RateLimiterExample {
    // 创建一个RateLimiter,每秒允许10个请求
    private static final RateLimiter rateLimiter = RateLimiter.create(10);

    public static boolean tryAcquire() {
        return rateLimiter.tryAcquire();
    }
}

在接口方法中,可以通过调用tryAcquire方法来判断是否允许请求通过:

java 复制代码
@RestController
public class ExampleController {
    @GetMapping("/example")
    public ResponseEntity<String> example() {
        if (!RateLimiterExample.tryAcquire()) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁,请稍后再试");
        }
        // 处理正常业务逻辑
        return ResponseEntity.ok("成功响应");
    }
}

另外,也可以使用Spring AOP(面向切面编程)结合自定义注解来实现更灵活的接口限流。通过自定义注解标记需要限流的接口,在切面类中使用限流逻辑对标记的接口进行拦截和处理,实现统一的限流控制。

防重放攻击

重放攻击是指攻击者截获并记录合法用户的有效请求,然后在稍后的时间重新发送这些请求,以达到欺骗系统的目的。这种攻击在涉及交易、数据修改等场景中危害较大,可能导致数据重复处理、资金损失等问题。

防重放攻击的方案

为了防止重放攻击,可以采用以下几种常见方案:

1、时间戳(timestamp) + 有效时间窗口:在请求中添加时间戳参数,服务器接收到请求后,判断时间戳与当前时间的差值是否在有效时间窗口内(例如5分钟)。如果超出窗口,则认为请求已过期,拒绝处理。这种方式可以有效防止攻击者在较长时间后重放请求,但对于短时间内的重放攻击防护较弱。

2、随机数(nonce)去重机制:请求中携带一个唯一的随机数(nonce),服务器记录每次请求的 nonce 值。当接收到新请求时,检查该nonce是否已存在。若存在,则判定为重复请求,拒绝处理。为了避免存储大量nonce值导致内存占用过高,可以结合时间戳,仅存储有效时间窗口内的nonce值。

防止重放攻击示例

java 复制代码
public class ReplayAttackInterceptor implements HandlerInterceptor {
    private static final Set<String> nonceSet = ConcurrentHashMap.newKeySet();
    private static final long EXPIRE_TIME = 5 * 60; // 5分钟有效期,单位秒

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String appId = request.getHeader("appId");
        String nonce = request.getHeader("nonce");
        String timestamp = request.getHeader("timestamp");

        if (appId == null || nonce == null || timestamp == null) {
            response.setStatus(HttpStatus.BAD_REQUEST.value());
            returnfalse;
        }

        long currentTime = System.currentTimeMillis() / 1000;
        if (currentTime - Long.parseLong(timestamp) > EXPIRE_TIME) {
            response.setStatus(HttpStatus.REQUEST_TIMEOUT.value());
            returnfalse;
        }

        String key = appId + nonce;
        if (nonceSet.contains(key)) {
            response.setStatus(HttpStatus.CONFLICT.value());
            returnfalse;
        }

        nonceSet.add(key);
        // 设置过期时间,避免nonceSet无限增长
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        executorService.schedule(() -> nonceSet.remove(key), EXPIRE_TIME, TimeUnit.SECONDS);
        executorService.shutdown();

        returntrue;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 处理后逻辑,可空
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 完成后逻辑,可空
    }
}

注册拦截器:

java 复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ReplayAttackInterceptor())
               .addPathPatterns("/**"); // 拦截所有接口
    }
}

签名验证

为什么需要签名机制

在接口调用过程中,签名机制用于验证请求的合法性和完整性,防止接口被恶意调用、参数被篡改等问题。常见的安全风险包括:

1、接口被恶意刷爆:攻击者伪造大量请求,不断调用接口,导致服务器资源耗尽。

2、请求参数被篡改:中间人在请求传输过程中修改请求参数,获取非法利益。

3、敏感参数泄露:接口参数暴露,可能导致敏感信息泄露,如用户密码、交易金额等。

通过签名校验,可以实现以下目标:

1、鉴别调用者身份:确保请求来自合法的调用方。

2、验证数据完整性:防止参数在传输过程中被篡改。

3、阻止重复请求:结合其他机制,如防重放攻击,进一步保障接口安全。

签名方案设计思路

签名机制的核心是对一组参数和密钥进行加密,服务器通过验签判断请求的合法性。以下是一个常见的签名方案设计流程:

签名参数设计:

appId:调用方身份标识,用于唯一识别调用方。

timestamp:请求时间戳,用于防止重放攻击。

nonce:随机字符串,增加签名的唯一性,与timestamp共同防止重放攻击。

sign:签名结果,由其他参数和密钥经过特定加密算法生成。

签名算法流程:

1、客户端发起请求时,将业务参数与公共参数(appId、timestamp、nonce)组成有序的Map。

2、将Map中的参数按key进行排序,拼接成key=value的形式,参数之间使用特定符号(如&)连接。

3、在拼接结果的末尾追加appSecret(仅服务端和调用方知晓的密钥)。

4、对拼接后的字符串进行MD5、SHA等加密算法处理,生成最终的sign。

5、服务器端收到请求后,从请求头或参数中读取appId,根据appId获取对应的appSecret。

6、服务器按照与客户端相同的规则,对接收到的参数进行排序、拼接、追加appSecret并加密,生成serverSign。

7、比对客户端传来的sign和服务器生成的serverSign,若一致则请求合法,否则拒绝请求。

实现签名验证示例

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SignCheck {
    boolean required() default true;
}

public class SignCheckInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            returntrue;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        SignCheck signCheck = handlerMethod.getMethodAnnotation(SignCheck.class);
        if (signCheck == null ||!signCheck.required()) {
            returntrue;
        }

        String appId = request.getHeader("appId");
        String timestamp = request.getHeader("timestamp");
        String nonce = request.getHeader("nonce");
        String sign = request.getHeader("sign");

        if (appId == null || timestamp == null || nonce == null || sign == null) {
            response.setStatus(HttpStatus.BAD_REQUEST.value());
            returnfalse;
        }

        // 获取请求参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        Map<String, String> paramMap = new TreeMap<>();
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            paramMap.put(entry.getKey(), String.join(",", entry.getValue()));
        }

        // 拼接参数
        StringBuilder paramBuilder = new StringBuilder();
        for (Map.Entry<String, String> entry : paramMap.entrySet()) {
            paramBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        paramBuilder.append("appSecret=").append(getAppSecret(appId)); // 根据appId获取对应的appSecret

        // 计算签名
        String serverSign = calculateSign(paramBuilder.toString());

        if (!sign.equals(serverSign)) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            returnfalse;
        }

        returntrue;
    }

    private String calculateSign(String paramStr) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] digest = md.digest(paramStr.getBytes());
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    private String getAppSecret(String appId) {
        // 实际应用中,应从数据库或配置文件中获取对应的appSecret
        // 这里简单示例,返回固定值
        return"your_secret_key";
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 处理后逻辑,可空
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 完成后逻辑,可空
    }
}

总结

1、提供接口文档和签名规则:服务提供方编写详细的接口文档,明确接口的功能、请求参数、响应格式以及签名规则,包括所需的公共参数(appId、timestamp、nonce)、签名算法、appSecret的获取方式等,提供给调用方。

2、调用方实现签名逻辑:调用方的后端开发人员根据接口文档和签名规则,在其代码中实现签名生成逻辑。在每次调用接口前,按照规则生成签名,并将appId、timestamp、nonce和sign等参数添加到请求中。

3、前端调用后端并发起请求:调用方的前端页面通过调用自家后端接口,由后端代为签名并向服务提供方的接口发起请求。

4、服务提供方验签并返回结果:服务提供方的服务器接收到请求后,首先进行签名验证。如果签名验证通过,则处理业务逻辑,并返回相应的结果给调用方;如果签名验证失败或请求参数不合法,返回错误信息给调用方。

相关推荐
Code季风12 分钟前
Spring AOP 与事务管理进阶:传播行为原理与实战指南
java·spring boot·spring
GOATLong26 分钟前
传输层协议TCP
c语言·开发语言·网络·c++·网络协议·tcp/ip
wanhengidc43 分钟前
服务器被网络攻击后该如何进行处理?
运维·服务器·网络
武昌库里写JAVA1 小时前
vue+iview+i18n国际化
java·开发语言·spring boot·学习·课程设计
你我约定有三1 小时前
RabbitMQ--Springboot解决消息丢失
java·spring boot·rabbitmq·java-rabbitmq
学Linux的语莫2 小时前
k8s的nodeport和ingress
网络·rpc·kubernetes
网络~小白2 小时前
MSTP技术
网络
tang_jian_dong2 小时前
springboot + vue3 拉取海康视频点位及播放
spring boot·后端·音视频
黄团团2 小时前
SpringBoot连接Sftp服务器实现文件上传/下载(亲测可用)
服务器·spring boot·github
安 当 加 密2 小时前
守护汽车“空中升级“:基于HSM/KMS的安全OTA固件签名与验证方案
安全·汽车