需求场景
由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。
Api接口签名验证主要防御措施为以下几个:
- 请求发起时间得在限制范围内
- 请求的用户是否真实存在
- 是否存在重复请求
- 请求参数是否被篡改
项目路径
https://gitee.com/charles_ruan/easy-sign
代码实现
不同的客户端有着不同的appSecret
。
- 通过密钥可以为
不同的客户端(调用方) 分配不同的appSecret
,来区分不同客户端app(调用方)。 - 将获取到的appSecret 参与到
sign(签名)
的生成,保证了客户端的请求签名是由我们后台控制的。
定义切面,拦截带SignatureValidation
方法。
- 获取方法上的参数,存入
SortedMap
- 判断参数是否合法
- 判断appId是否存在对应的secret
- 判断时间戳是否有效
- 进行签名校验
@Slf4j
@Aspect
public class SignAspect {
@Before("@annotation(signatureValidation)")
public void doBefore(SignatureValidation signatureValidation) throws Throwable {
// 接收到请求,记录请求内容
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
SortedMap<String, String> allParams = HttpUtils.getAllParams(request);
// 1、获取请求sign签名参数,
String sign = allParams.get("sign");
if (StrUtil.isBlank(sign)) {
throw new RuntimeException("sign不能为空");
}
// 2、获取请求参数secret
String appId = allParams.get("appId");
String appSecret = getAppSecret(appId);
if (StrUtil.isBlank(appSecret)) {
throw new RuntimeException("appId不合法");
}
// 3、获取请求参数timestamp 时间戳,
String timestamp = allParams.get("timestamp");
if (StrUtil.isBlank(timestamp)) {
throw new RuntimeException("timestamp不能为空");
}
//3. 比较时间,120s内为合法请求
if (Math.abs(Long.parseLong(timestamp) - System.currentTimeMillis()) > 120000) {
throw new RuntimeException("timestamp失效");
}
allParams.put("secret", appSecret);
verifySign(allParams);
}
private void verifySign(SortedMap<String, String> allParams) {
// 对方签名
String sign = allParams.get("sign");
allParams.remove("sign");
String mySign = SecureUtil.md5(JSONUtil.toJsonStr(allParams)).toUpperCase();
log.info("验签,对方签名:{},我方签名:{}", sign, mySign);
// 验签
Assert.isTrue(StrUtil.equals(sign, mySign), "验签失败");
}
public String getAppSecret(String appId) {
Map<String, String> map = new HashMap<>();
map.put("zs001", "asd123fhg3b7fgh7dfg");
map.put("ls001", "hghfgh123btgfyh1212");
return map.get(appId);
}
}
利用nonce参数,可以防止重复提交,在签名验证成功后,判断是否重复提交,原理就是结合redis,判断是否已经提交过
public boolean isReplayAttack(String appId, String timeStamp, String nonce, String signature) {
StringBuilder redisKey = new StringBuilder();
redisKey.append("IS_REPLAY_ATTACK").append(":")
.append(Constant.APP_ID).append(":").append(appId)
.append(Constant.TIME_STAMP).append(":").append(timeStamp)
.append(Constant.NONCE).append(":").append(nonce)
.append(Constant.SIGN).append(":").append(signature);
Object value = redisTemplate.opsForValue().get(redisKey);
if (value != null && StringUtils.equals(signature, value.toString()))
return false;
else
redisTemplate.opsForValue().set(redisKey, signature, 1000 * 50);
return false;
}
标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符