API签名认证详解

API签名认证详解极其重要 严格保密 secretKey 只有客户端和服务端所知道 但是不参与请求 服务端存在 accessKey secretKey 的关系

大家好,欢迎来到IT知识分享网。

本质

accessKey secretKey / appKey appSecret 一样

1. 思考(场景)

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 存在的问题

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


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也会跟着改变

如何确保请求的唯一性

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/134683.html

(0)
上一篇 2025-07-10 16:45
下一篇 2025-07-10 17:10

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信