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

相关推荐
小码编匠1 天前
基于 Spring Boot + Vue 的轻量级进销存系统
vue.js·spring boot·后端
咖啡Beans1 天前
SpringBoot集成p6spy监控sql耗时
spring boot·mysql·spring cloud
编啊编程啊程1 天前
Netty从0到1系列之RPC通信
java·spring boot·rpc·kafka·dubbo·nio
Keepreal4961 天前
浏览器同源策略与跨域解决方案
安全·浏览器
xinxinhenmeihao1 天前
HTTP代理HTTP(S)、SOCKS5有哪些作用?
网络·网络协议·http
王大锤43911 天前
2种方式从springbean中获取bean实例
java·spring boot
mumu1307梦1 天前
SpringAI 实战:解决 Netty 超时问题,优化 OpenAiApi 配置
java·spring boot·netty·超时·timeout·openapi·springai
咖啡Beans1 天前
了解Mybatis拦截器
java·spring boot·mybatis
GZ_TOGOGO1 天前
华为HCIE认证-“天花板”级考试的难度解析
网络·华为认证
努力学习的小廉1 天前
深入了解linux网络—— UDP网络通信
linux·网络·udp