API签名认证详解

本质

签发签名
认证签名(使用签名或校验码。这就像一些短信接口的 key 一样 别纠结名字)

accessKey secretKey / appKey appSecret 一样

1. 思考(场景)

如果说我们把这个接口提供给开发者,但是我们现在是不是根 本不知道是谁来调用的。假如说我们的服务器只能允许100个 人来调用。假如说有一个攻击者来了,他就刷量了,他想疯狂 的刷我的服务器,那是不是非常的不安全? 另外一方面就是你的服务器的性能肯定会受到损耗,也会影响 正常用户。所以我们肯定要给这个接口,要去做一些保护。比 如说每个用户只能去每秒只能调用十次,这种我们要去做一个 限额。而且后续可能你做大了还要去做收费,对不对?所以我 们肯定要知道是谁来调用的这个接口,而且不能让随便一个没 有权限的人都来找你。 所以,我们怎么知道是谁调用的接口?
即 保证安全性不能随便一个人就能调用

2. 为什么选择 API 签名认证

  1. 保证安全性,不能让任何人都能调用接口
  2. 适用于无需保存登录态的场景。只认签名,不关注用户登录态。(不关心你是男是女,只要你有 “证” 就行)

3. 实现

3.1 通过 HTTP request header 头传参

参数1:accessKey:调用的标识(尽量复杂)
参数2:secretKey:密钥
`(类似于用户名、密码,区别:ak\sk无状态)

根据 accessKey 识别调用者的身份
根据 secretKey 识别出签名是否合法

在数据库用户表中新增两个字段 accessKey和secretKey,让用户每次发送请求时,将accessKey 和 secretKey 添加在请求头上一发送

3.1.1 demo 实现(使用 hutool 简介 | Hutool)

  1. 创建 danhuaapiClient 客户端,并加入两个变量、并创建构造函数
private String accessKey;  
  
private String secretKey;  
  
public danhuaapiClient(String accessKey, String secretKey) {  
    this.accessKey = accessKey;  
    this.secretKey = secretKey;  
}
  1. 客户端发送请求时携带请求头
    .addHeaders(getHeaders())
private Map<String, String> getHeaders() {  
    Map<String, String> headers = new HashMap<>();  
    headers.put("accessKey", accessKey);  
    headers.put("secretKey", secretKey);  
    return headers;  
}
public String postNameJson(User user){  
    String json = JSONUtil.toJsonStr(user);  
    HttpResponse execute = HttpRequest.post("http://localhost:8123/api/name/user")  
            .addHeaders(getHeaders())  
            .body(json)  
            .execute();  
    System.out.println("execute :" + execute.getStatus());  
    String body = execute.body();  
    System.out.println("body :" + body);  
    return body;  
}

  1. 编写测试类
String accessKey = "aikun";  
String secretKey = "hd9ehxjue";  
danhuaapiClient client = new danhuaapiClient(accessKey, secretKey);  
String name1 = client.postNameJson(new User("篮球", "ctrl"));  
System.out.println(name1);
  1. 服务端校验
    这里从请求头里面获取信息,这里的校验只是测试,真实情况应该 是从数据库中去取。
@PostMapping("/user")  
public String postNameJson(@RequestBody User user, HttpServletRequest request){  
    String accessKey = request.getHeader("accessKey");  
    String secretKey = request.getHeader("secretKey");  
    if (!"aikun".equals(accessKey) || !"hd9ehxjue".equals(secretKey)){  
        throw new RuntimeException("无权限! 小黑子能不能不要再黑我家哥哥啦~");  
    }  
    return "post 方法 json 参数 用户的名字: " + user.getUsername();  
}

3.1.2 存在的问题

如果仅仅通过参数1,参数2放在请求头的方式进行传递,那么
别人很可能通过重放(二次请求,黑客通过抓包获取到了请求的HTTP报文,然后黑客自己编写了一个类似的HTTP请求,发送给服务器。也就是说服务器处理了两个请求,先处理了正常的HTTP请求,然后又处理了黑客发送的篡改过的HTTP请求)等方式破解
或者拦截你的请求
就如同get请求登录,你把用户名和密码直接拼在地址后面,这肯定是不安全的

重放
f12 -> 网络 -> 选择一个请求 重放XHR

记住!
密码千万不能把放在服务器之间传递
一般是根据密钥生成签名sign


3.2 优化校验

既然以上方法是因为 accessKey 和 secretKey 是明文所导致的,那能不能像,用户登录一样,将secretKey 进行加密,而且采用单向加密(md5,sha256等等)

3.2.1 参数增加

参数3:用户请求参数
参数4:sign 签名

3.2.2 具体做法

假如我们有用户参数,我们用密钥和他拼接,用签名算法得到一个 不可解密的值
用户参数+密钥 => 签名生成算法(MD5,HMac,Sha1) => 不可解 密的值
例子: abc+abcdefg => deskdjhs(得到的值是随机的)

怎么知道签名对不对?
服务器用一模一样的参数和算法去生成签名,只要和用户传的一 致,就标识密钥一致

怎么防止重放?(怎么防止别人拿我们之前发布的请求信息以后再重新再发一次)

  1. 加一个 参数5 nonce 随机数 随机数就我们每次请求的时候发一个随机数。后端记录。后端它只认一 次,后端只接受并认可该随机数一次,一旦被使用过,将不再接受 相同的随机数,解决了重放问题。但后端要保存用过 的随机数,如果接口并发量大,我们需要处理大量的请求,还要定时清理
  2. 加一个 参数6 timestamp 时间戳,校验时间戳是否过期 发请求时携带一个时间戳,后端会验证其是否在指定的时间范围, 可以控制随机数的过期时间。后端需要同时验证这两个参数,只要时间戳过期或随机数被使用过,后端就会拒绝请求
  3. 这两个参数还可以联合起来,nonce 不是要定期清理吗,timestamp 过时了就不能用了,那不是就可以根据 timestamp 时间戳的时间进行定期清理 建议redis ,redis 在存储Key-value 本身就可以设置过期时间

3.2.3 实现

  1. 编写生成 sign 方法的工具类
public static String genSign(String body,String secretKey) {  
    Digester md5=new Digester(DigestAlgorithm.SHA256);  
    String content = body + '*' + secretKey;  
    return md5.digestHex(content);  
}
  1. 添加参数
   private Map<String, String> getHeaders(String body) {  
        Map<String, String> headers = new HashMap<>();  
        headers.put("accessKey", accessKey);  
//        headers.put("secretKey", secretKey);  
        headers.put("nonce", RandomUtil.randomNumbers(4));  
        headers.put("body", body);  
        headers.put("timestamp", String.valueOf(System.currentTimeMillis()/1000));  
        headers.put("sign", SignUtil.genSign(body, secretKey));  
        return headers;  
    }  
  
  
  
    public String postNameJson(User user){  
        String json = JSONUtil.toJsonStr(user);  
        HttpResponse execute = HttpRequest.post("http://localhost:8123/api/name/user")  
                .addHeaders(getHeaders(json))  
                .body(json)  
                .execute();  
        System.out.println("execute :" + execute.getStatus());  
        String body = execute.body();  
        System.out.println("body :" + body);  
        return body;  
    }
  1. 参数校验
@PostMapping("/user")  
public String postNameJson(@RequestBody User user, HttpServletRequest request) {  
    String accessKey = request.getHeader("accessKey");  
    String nonce = request.getHeader("nonce");  
    String timestamp = request.getHeader("timestamp");  
    String sign = request.getHeader("sign");  
    // 这段代码将从 ISO-8859-1 解码得到的字节流再用 UTF-8 编码,以适应中文字符。  
    String body = new String(request.getHeader("body").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);  
    //校验时间戳  
    if (System.currentTimeMillis() / 1000 - Long.parseLong(timestamp) > 60 * 5) {  
        throw new RuntimeException("无权限! 小黑子能不能不要再黑我家哥哥啦~");  
    }  
    // todo 实际上应该去数据库查询  
    if (!"aikun".equals(accessKey)) {  
        throw new RuntimeException("无权限! 小黑子能不能不要再黑我家哥哥啦~");  
    }  
    // todo 这里不校验随机数了  
    if (Long.parseLong(nonce) > 10000) {  
        throw new RuntimeException("无权限! 小黑子能不能不要再黑我家哥哥啦~");  
    }  
    String serverSign = SignUtil.genSign(body, "hd9ehxjue");  
    if (!sign.equals(serverSign)) {  
        throw new RuntimeException("无权限! 小黑子能不能不要再黑我家哥哥啦~");  
    }  
  
    return "post 方法 json 参数 用户的名字: " + user.getUsername();  
}

总结

如何确定访问者的合法性

accessKey secretKey(极其重要,严格保密)
secretKey只有客户端和服务端所知道,但是不参与请求,服务端存在 accessKey -> secretKey 的关系

参数不被修改

sign根据参数,利用算法得出sign,如果你一旦修改一个参数,那么sign也会跟着改变

如何确保请求的唯一性

nonce 随机数
timestamp 时间戳

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值