基于缓存注解的时间戳令牌防重复提交设计

文章目录

一,概述

API接口由于需要供第三方服务调用,所以必须暴露到外网,并提供了具体请求地址和请求参数。为了防止重放攻击必须要保证请求仅一次有效

比较成熟的做法有批量颁发时间戳令牌,每次请求消费一个令牌

二,实现过程

下面我们基于本地缓存caffeine来说明具体实现。

1、引入pom依赖

xml 复制代码
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>
		
		<!-- caffeine依赖 -->
		<dependency>
			<groupId>com.github.ben-manes.caffeine</groupId>
			<artifactId>caffeine</artifactId>
		</dependency>

2、定义缓存管理

java 复制代码
import java.util.concurrent.TimeUnit;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.github.benmanes.caffeine.cache.Caffeine;

/**
 * 
 * CacheConfig
 * 
 * @author 00fly
 * @version [版本号, 2019年12月18日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Configuration
public class CacheConfig extends CachingConfigurerSupport
{
    @Bean
    @Override
    public CacheManager cacheManager()
    {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // 方案一(常用):定制化缓存Cache
        cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).initialCapacity(100).maximumSize(10000));
        // 如果缓存种没有对应的value,通过createExpensiveGraph方法同步加载 buildAsync是异步加载
        // .build(key -> createExpensiveGraph(key))
        
        // 方案二:传入一个CaffeineSpec定制缓存,它的好处是可以把配置方便写在配置文件里
        // cacheManager.setCaffeineSpec(CaffeineSpec.parse("initialCapacity=50,maximumSize=500,expireAfterWrite=5s"));
        return cacheManager;
    }
}

3、时间戳服务类

注意:一定要理解为什么使用SpringContextUtils

java 复制代码
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.LongStream;

import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import com.fly.core.utils.SpringContextUtils;

/**
 * TimestampService
 */
@Service
public class TimestampService
{
    /**
     * 批量获取用户timestamp,支持缓存
     */
    @Cacheable(value = "timestamp", key = "#user", unless = "#result.size()==0")
    public List<Long> batchGet(String user)
    {
        String userId = DigestUtils.md5DigestAsHex(user.getBytes(StandardCharsets.UTF_8));
        if (StringUtils.isBlank(userId))
        {
            throw new RuntimeException("用户不存在");
        }
        return LongStream.range(0, 10).map(i -> System.currentTimeMillis() + i).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
    }
    
    /**
     * 判断用户timestamp是否有效
     */
    public boolean isFirstUse(String user, Long timestamp)
    {
        // 注意:缓存基于代理实现,直接调用,缓存机制会失效
        TimestampService timestampService = SpringContextUtils.getBean(TimestampService.class);
        List<Long> data = timestampService.batchGet(user);
        boolean isFirstUse = data.contains(timestamp);
        if (isFirstUse)
        {
            timestampService.removeThenUpdate(user, timestamp);
        }
        return isFirstUse;
    }
    
    /**
     * 移除用户已使用的timestamp,刷新缓存
     * 
     */
    @CachePut(value = "timestamp", key = "#user")
    public List<Long> removeThenUpdate(String user, Long timestamp)
    {
        // 注意:缓存基于代理实现,直接调用,缓存机制会失效
        TimestampService timestampService = SpringContextUtils.getBean(TimestampService.class);
        List<Long> data = timestampService.batchGet(user);
        data.remove(timestamp);
        if (data.size() < 5) // 及时补充
        {
            data.addAll(batchGet(user));
        }
        return data;
    }
}

4、模拟测试接口

java 复制代码
import java.util.Collections;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.fly.core.entity.JsonResult;
import com.fly.openapi.service.TimestampService;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Api(tags = "接口辅助")
@RestController
@RequestMapping("/auto/help")
public class AutoHelpController
{
    @Autowired
    TimestampService timestampService;
    
    @ApiOperation("批量获取用户timestamp")
    @GetMapping("/getBatchTimestamps")
    public JsonResult<?> getBatchTimestamps(@RequestParam String user)
    {
        log.info("getBatchTimestamps for {}", user);
        return JsonResult.success(Collections.singletonMap("timestamps", timestampService.batchGet(user)));
    }
    
    @ApiOperation("消费timestamp")
    @GetMapping("/useTimestamp")
    public JsonResult<?> useTimestamp(@RequestParam String user, Long timestamp)
    {
        log.info("useTimestamp for {}", user);
        return JsonResult.success(Collections.singletonMap("isFirstUse", timestampService.isFirstUse(user, timestamp)));
    }
}

三,测试过程

1, 模拟批量获取

输入用户名00fly

2, 消费令牌



四,源码放送

https://gitcode.com/00fly/springboot-openapi

bash 复制代码
git clone https://gitcode.com/00fly/springboot-openapi.git

五,优化方向

  1. 批量获取令牌可采用https、tcp、grpc等更加安全的协议获的
  2. 获取令牌可以考虑采用非对称加密算法鉴权
  3. 多实例部署,可切换到分布式缓存,如redis

有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!

-over-

相关推荐
海兰14 小时前
【高速缓存】RedisVL 存储类型选择指南:Hash 与 JSON
人工智能·redis·算法·缓存·json·哈希算法
YOU OU1 天前
Redis事务
数据库·redis·缓存
她说可以呀1 天前
Redis集群
数据库·redis·缓存
无小道1 天前
深入解析操作系统文件缓冲区:页缓存、基数树与f_pos的协同设计
linux·缓存·文件缓冲区
無a伟2 天前
redis采用的单线程模型,为什么这么快?
数据库·redis·缓存
消失的旧时光-19432 天前
第一篇:Redis 到底是什么?从进程、内存和通信开始讲清楚
java·数据库·redis·缓存
落落Plus2 天前
浏览器缓存机制详解
javascript·缓存·浏览器缓存
心机之蛙qee2 天前
Redis的主从、哨兵及集群
数据库·redis·缓存
qq_297574672 天前
RuoYi框架二次开发系列(五)性能优化、Redis缓存实战、分布式部署、接口限流与安全加固生产方案
redis·缓存·性能优化
老刘说AI2 天前
SGLang 深度优化: Radix 缓存与复杂任务的极致吞吐
人工智能·神经网络·机器学习·缓存·架构·sglang