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

文章目录

一,概述

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-

相关推荐
段帅龙呀2 小时前
Redis构建缓存服务器
服务器·redis·缓存
夜斗小神社17 小时前
【黑马点评】(二)缓存
缓存
Hello.Reader1 天前
Redis 延迟监控深度指南
数据库·redis·缓存
Hello.Reader1 天前
Redis 延迟排查与优化全攻略
数据库·redis·缓存
在肯德基吃麻辣烫2 天前
《Redis》缓存与分布式锁
redis·分布式·缓存
先睡2 天前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
CodeWithMe3 天前
【Note】《深入理解Linux内核》 Chapter 15 :深入理解 Linux 页缓存
linux·spring·缓存
大春儿的试验田3 天前
高并发收藏功能设计:Redis异步同步与定时补偿机制详解
java·数据库·redis·学习·缓存
likeGhee3 天前
python缓存装饰器实现方案
开发语言·python·缓存
C182981825753 天前
OOM电商系统订单缓存泄漏,这是泄漏还是溢出
java·spring·缓存