spring cloud脚手架项目(十六)开发HTTP API 接口签名验证

前言

最近有给写一个开发接口给第三方用。也趁机学习了一下有关如果提供一个接口的验签的过程
也有参考到这篇文章:https://mp.weixin.qq.com/s/VJX6h2Fl_a5N3iCWe777nA

如何接口安全问题

需要解决3个问题:
请求身份是否合法?
请求参数是否被篡改?
请求是否唯一?

请求身份

为开发者分配accessKey(开发者标识,确保唯一)和secretKey(用于接口加密,确保不易被穷举,生成算法不易被猜测)。

防止篡改

按照请求参数名的字母升序排列非空请求参数(包含AccessKey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA;
在stringA最后拼接上Secretkey得到字符串stringSignTemp;
对stringSignTemp进行MD5运算,并将得到的字符串所有字符转换为大写,得到sign值。
请求携带参数accessKey和sign,只有拥有合法的身份accessKey和正确的签名sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到secretKey(仅作本地加密使用,不参与网络传输),无法伪造合法的请求。

重放攻击

虽然解决了请求参数被篡改的隐患,但是还存在着重复使用请求参数伪造二次请求的隐患。
使用
timestamp+nonce方案
nonce指唯一的随机字符串,用来标识每个被签名的请求。通过为每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用(记录所有用过的nonce以阻止它们被二次使用)。
然而,对服务器来说永久存储所有接收到的nonce的代价是非常大的。可以使用timestamp来优化nonce的存储。
假设允许客户端和服务端最多能存在15分钟的时间差,同时追踪记录在服务端的nonce集合。当有新的请求进入时,首先检查携带的timestamp是否在15分钟内,如超出时间范围,则拒绝,然后查询携带的nonce,如存在已有集合,则拒绝。否则,记录该nonce,并删除集合内时间戳大于15分钟的nonce。

代码实现

基本请求类

public class OpenRequestDTO {
    private String accessKey;
    private String sign;
    private LocalDateTime timestamp;
    private String nonce;
    //以下为get/set方法

继承后的一个实现类

public class TestOpenRequestDTO extends OpenRequestDTO{
    private String paramA;
    private String paramB;
    //以下为get/set方法

具体的参数校验方法类

import com.chen.service.requestDTO.OpenRequestDTO;
import com.chen.service.requestDTO.TestOpenRequestDTO;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gson.Gson;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.BeanUtils;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class CheckSign {
    private static String secretKey = "123456";
    private static String accessKey = "testUser";

    // 通过CacheBuilder构建一个缓存实例
    private static Cache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(10000) // 设置缓存的最大容量
            .expireAfterWrite(15, TimeUnit.MINUTES) // 设置缓存在写入一分钟后失效
            .concurrencyLevel(10) // 设置并发级别为10
            .recordStats() // 开启缓存统计
            .build();

    public static void main(String[] args) {
        TestOpenRequestDTO openRequestDTO = new TestOpenRequestDTO();
        openRequestDTO.setAccessKey(accessKey);
        openRequestDTO.setNonce("qwe");
        //先执行一次获得对应的sign
        openRequestDTO.setSign("F36A9A4436D5C8B42A410C6A061FE333");
        openRequestDTO.setParamA("a");
        openRequestDTO.setParamB("b");
        //注意设置时间与当前时间
        LocalDateTime dateTime = LocalDateTime.of(2020, 11, 20, 11, 40, 0);
        openRequestDTO.setTimestamp(dateTime);
        OpenRequestDTO openRequestDTONext = new OpenRequestDTO();
        BeanUtils.copyProperties(openRequestDTO,openRequestDTONext);
        Gson gson = new Gson();
        String a = gson.toJson(openRequestDTO);
        System.out.println("请求json:"+a);
        System.out.println(CheckSign.check(openRequestDTO));
        System.out.println(CheckSign.check(openRequestDTONext));
    }

    public static boolean check(OpenRequestDTO openRequestDTO) {
        if (!checkParams(openRequestDTO)) {
            return false;
        }
        if (!checkTime(openRequestDTO)) {
            return false;
        }
        if (!checkNonce(openRequestDTO)) {
            return false;
        }
        String sign = openRequestDTO.getSign();
        openRequestDTO.setSign(null);
        Gson gson = new Gson();
        String a = gson.toJson(openRequestDTO);
        Map<String, Object> map = gson.fromJson(a, Map.class);
        String result = CheckSign.sign(map, secretKey);
        System.out.println("result:" + result);
        if (result.equals(sign)) {
            return true;
        }
        return false;
    }

    /**
     * 参数校验
     * @param openRequestDTO
     * @return
     */
    public static boolean checkParams(OpenRequestDTO openRequestDTO) {
        if (openRequestDTO.getAccessKey() == null || openRequestDTO.getNonce() == null || openRequestDTO.getSign() == null || openRequestDTO.getTimestamp() == null) {
            System.out.println("校验失败,有参数为空");
            return false;
        }
        return true;
    }

    /**
     * 时间校验
     * @param openRequestDTO
     * @return
     */
    public static boolean checkTime(OpenRequestDTO openRequestDTO) {
        Duration duration = Duration.between(openRequestDTO.getTimestamp(), LocalDateTime.now());
        if (duration.toMinutes() > 15 || duration.toMinutes() < 0) {
            System.out.println("校验失败,时间超时");
            return false;
        }
        return true;
    }

    /**
     * 随机数校验
     * @param openRequestDTO
     * @return
     */
    public static boolean checkNonce(OpenRequestDTO openRequestDTO) {
        String key = openRequestDTO.getNonce();
        String value = null;
        value = cache.getIfPresent(key);
        if (value != null) {
            System.out.println("校验失败,重复请求");
            return false;
        }
        cache.put(openRequestDTO.getNonce(), openRequestDTO.getTimestamp().toString());
        return true;
    }

    /**
     * 签名校验
     * @param attrs
     * @param signKey
     * @return
     */
    public static String sign(Map<String, Object> attrs, String signKey) {
        StringBuilder sb = new StringBuilder();
        Set<String> strings = attrs.keySet();
        ArrayList<String> keys = new ArrayList<>(strings);
        Collections.sort(keys);
        for (String key : keys) {
            Object value = attrs.get(key);
            buildField(sb, key, value);
        }
        sb.append(signKey);
        System.out.println("最终参数为sb:" + sb);
        return DigestUtils.md5Hex(sb.toString().getBytes(StandardCharsets.UTF_8)).toUpperCase();
    }

    /**
     * 参数构建
     * @param sb
     * @param key
     * @param value
     */
    private static void buildField(StringBuilder sb, String key, Object value) {
        if (null != value) {
            //第二次
            if (sb.length() > 0) {
                sb.append("&");
            }
            sb.append(key);
            sb.append("=");
            sb.append(value);
        }
    }
}


结果

请求json:{"paramA":"a","paramB":"b","accessKey":"testUser","sign":"F36A9A4436D5C8B42A410C6A061FE333","timestamp":{"date":{"year":2020,"month":11,"day":20},"time":{"hour":11,"minute":40,"second":0,"nano":0}},"nonce":"qwe"}
最终参数为sb:accessKey=testUser&nonce=qwe&paramA=a&paramB=b&timestamp={date={year=2020.0, month=11.0, day=20.0}, time={hour=11.0, minute=40.0, second=0.0, nano=0.0}}123456
result:F36A9A4436D5C8B42A410C6A061FE333
true
校验失败,重复请求
false

说明

对应的jar包

spring工具
google的工具包
jdk1.8
google的gsong工具

参数设置

secretKey恒定为123456
accessKey恒定为testUser
这部分在服务器上可以用配置文件或者存放在数据库中
设置了一个google的缓存,用于存储请求的随机唯一值,在线上服务器可以用redis代替

代码使用

先localdatetime设置时间不能和现在时间超出15分钟。
直接启动main方法获得sign值,这个时候会提示校验出错,
获得产生的sign值进行修改
然后校验第一次通过,第二次由于随机数相同校验失败

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值