背景
API接口由于需要供第三方服务调用,所以必须暴露到外网,并提供了具体请求地址和请求参数
为了防止被第别有用心之人获取到真实请求参数后再次发起请求获取信息,需要采取很多安全机制;
安全策略
- 1.首先: 需要采用https方式对第三方提供接口,数据的加密传输会更安全,即便是被破解,也需要耗费更多时间
- 2.其次:需要有安全的后台验证机制【本文重点】,达到防参数篡改+防二次请求,防止重放攻击必须要保证请求仅一次有效;
防参数篡改
客户端使用约定好的秘钥对传输参数进行加密,得到签名值signature,并且将签名值也放入请求参数中,发送请求给服务端
服务端接收客户端的请求,然后使用约定好的秘钥对请求的参数(除了signature以外)再次进行签名,得到签名值autograph。
服务端对比signature和autograph的值,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,认定为非法请求
防二次请求
- 基于timestamp的方案
- 基于nonce的方案
- 基于timestamp和nonce的方案(推荐)
pig整合anti-replay
pig-upms-biz添加依赖
<dependency>
<groupId>com.pig4cloud.plugin</groupId>
<artifactId>anti-replay-spring-boot-starter</artifactId>
<version>0.0.2</version>
</dependency>
添加配置文件,注意也要配置redis
anti:
replay:
signature-algorithm:
salt: 67236618ad696de2a91700b1afda43d0 #盐值 加密生成signature时用到
request:
expire-time: 10 #请求过期时间
cache:
cache-key-prefix: NARUTO:SECURITY:ANTI-REPLAY:REQUEST_ID_
lock-hold-time: 30 #锁释放时间
header-key:
signature: mySignature # 与请求头参数名对应
nonce: mtNonce # 与请求头参数名对应
timestamp: myTimestamp # 与请求头参数名对应
url: myUrl # 与请求头参数名对应
token: myToken # 与请求头参数名对应
方法添加@NarutoAntiReplay注解,启动服务使用postman进行测试。注意请求头需要携带的参数。其中signature的值,为请求配置文件中配置的salt值+请求头参数值nonce+真实请求的url+请求头参数值timestamp+请求头参数值token进行md5加密后的值。注意,如果方法带有参数,对参数也应该加密。具体逻辑可以查看MdFiveUtils的digest方法。md5加密,pig-ui可以使用CryptoJS.MD5进行加密。只有通过验证,才可访问方法。
@NarutoAntiReplay
@GetMapping("/demo/test")
public R<Void> test(){
log.info("test.....");
return R.ok(null,"ojbk");
}
原理
首先,我们引入naruto-anti-replay-spring-boot-starter依赖,实际上是通过自动装配了NarutoAntiReplayAutoConfiguration这个配置类。
这是该配置类的源码:
- 其中EnableBeanFactory注解通过import向容器注入了BeanFactory这个类。
BeanFactory这个类主要是通过继承ApplicationContextAware,通过静态变量ApplicationContext获得了获取容器中bean的能力。
- EnableNarutoAntiReplay注解向容器import了NarutoAntiReplaySpringConfiguration这个配置类,
该配置类向容器import了四个类- BeanFactory
BeanFactory这个类主要是通过继承ApplicationContextAware,通过静态变量ApplicationContext获得了获取容器中bean的能力 - ReplayProperties
ReplayProperties是读取前缀为naruto.security.api.anti-replay配置的配置类。 - CacheSupport
CacheSupport 主要是封装了redisTemplate,实现了对缓存的一些操作。 - AntiReplayAspect
该类通过Aspect注解开启切面,around方法对添加NarutoAntiReplay注解的方法进行环绕处理。
首先,如果接口方法上NarutoAntiReplay注解的checkSignature值为true,程序会进行签名验证。签名校验再SignatureValidator这个类中进行,该类主要是获取请求头中的timestamp、url、token、以及方法入参,将这几个参数进行加密,与请求头携带的signature进行比较,如果不匹配,会抛出异常,表示数据签名验证未通过。
数据签名验证通过以后,如果NarutoAntiReplay注解的antiReplay值为true的话,会对请求路径进行判断,禁止重复请求校验是在AntiReplayValidator这个类进行的,会判断请URL与实际请求路径是否相符,请求是否已过期。然后通过redis的自增操作,判断请求是否重复提交。
如果都通过,放发继续执行。
综上,这是AntiReplayAspect这个切面类的主要功能。
- BeanFactory
代码:
/**
* @description 禁止重复请求切面
* @author XQF.Sui
* @date 2021/07/30 01:15
*/
@Order(value = 1)
@Aspect
@Component
public class AntiReplayAspect {
private final ReplayProperties props;
private final String servletContextPath;
AntiReplayAspect(ReplayProperties props, Environment env) {
this.props = props;
this.servletContextPath = env.getProperty("server.servlet.context-path");
}
@Around(value = "@annotation(narutoSecurity)")
public Object around(ProceedingJoinPoint point, NarutoAntiReplay narutoSecurity)
throws Throwable {
HttpServletRequest request = getHttpServletRequest();
// 签名验证
if (narutoSecurity.checkSignature()) {
SignatureValidator.builder().arguments(point.getArgs()).data(request).execute();
}
// 禁止重放验证
if (narutoSecurity.antiReplay()) {
// 请求路径
String targetUrl = StringUtils.replace(request.getRequestURI(), servletContextPath, "");
try (AntiReplayValidator.AntiReplayWorker worker =
AntiReplayValidator.builder()
.methodName(SpringUtils.getMethodName(point))
.nonce(request.getHeader(props.getHeaderKey().getNonce()))
.url(request.getHeader(props.getHeaderKey().getUrl()))
.targetUrl(targetUrl)
.timestamp(
ConvertUtils.StringToLong(
request.getHeader(props.getHeaderKey().getTimestamp())))) {
worker.execute();
return point.proceed();
}
}
return point.proceed();
}
private HttpServletRequest getHttpServletRequest() {
return getServletRequestAttributes().getRequest();
}
private ServletRequestAttributes getServletRequestAttributes() {
return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
}
}