一,概述
API接口由于需要供第三方服务调用,所以必须暴露到外网,并提供了具体请求地址和请求参数。为了防止重放攻击必须要保证请求仅一次有效
比较成熟的做法有批量颁发时间戳令牌,每次请求消费一个令牌
。
二,实现过程
下面我们基于本地缓存caffeine
来说明具体实现。
1、引入pom依赖
<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、定义缓存管理
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
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、模拟测试接口
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
git clone https://gitcode.com/00fly/springboot-openapi.git
五,优化方向
- 批量获取令牌可采用https、tcp、grpc等更加安全的协议获的
- 获取令牌可以考虑采用非对称加密算法鉴权
- 多实例部署,可切换到分布式缓存,如redis
有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-