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

文章目录

一,概述

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-

相关推荐
SPC的存折4 小时前
1、Redis数据库基础
linux·运维·服务器·数据库·redis·缓存
身如柳絮随风扬11 小时前
Redis如何实现高效插入大量数据
数据库·redis·缓存
予早12 小时前
Redis 设置库的数量
数据库·redis·缓存
黑金IT12 小时前
vLLM本地缓存实战,重复提交直接复用不浪费算力
人工智能·缓存
Rick199314 小时前
Redis查询为什么快
数据库·redis·缓存
Rick199315 小时前
Redis 底层架构图
数据库·redis·缓存
Arva .16 小时前
Redis 数据类型
数据库·redis·缓存
笑我归无处16 小时前
Redis和数据库的数据一致性问题研究
数据库·redis·缓存
小红的布丁17 小时前
操作系统与高性能 IO:零拷贝、一次读 IO、CPU 缓存与伪共享
缓存
SPC的存折17 小时前
(自用)LNMP-Redis-Discuz5.0部署指南-openEuler24.03-测试环境
linux·运维·服务器·数据库·redis·缓存