API 签名认证
学习目标
在这里,你将系统学习了解 API 签名认证
的具体代码实现
我们将以最简单直接
的方式为您呈现内容!
# 🍜 需求分析
我们已经自主设计了丰富的接口,可以根据用户请求提供丰富的接口服务。那么当用户发起一次接口调用请求之后,我们应该如何妥善处理这个请求,才能保证这次接口调用的完整性和安全性呢?
我们希望实现这样的业务逻辑:
每一个在 MemoryAPI 忆汇廊 注册过的用户,系统都会为其发放唯一的密钥,用户可以发起接口调用请求时必须
携带密钥
服务端在接收到用户的接口调用请求之后,需要根据该请求中携带的密钥,来判断该用户是否有权限享受接口调用服务。
对于携带密钥的校验,可以从很多方面着手考虑:密钥是否正确、如何确保密钥的完整性等等,这都是判断该用户能否成功调用接口的重要依据
当然,除了以上校验密钥的核心逻辑,一次完整的接口调用业务逻辑判断还应该有:该用户是否存在、所调用的接口是否存在或者是否上线、给用户分配的调用次数是否耗光等等,这样的校验逻辑很大程度上能使整个接口调用流程更加完善,目前不做考虑。
# 🍚 API 签名认证
🍖 推荐阅读:网络传输安全 (opens new window)
# 原理
- API 签名认证的原理是通过使用密钥对请求进行签名,以验证请求的来源和完整性。目标是确保只有拥有正确密钥的请求才能通过验证,防止未经授权的访问和使用,以及检测数据篡改。
# 防止篡改,完整性
摘要算法:一种特殊的单向加密的压缩算法,它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。摘要和原数据是完全等价的,加密后的数据无法解密,不能从摘要逆推出原文。
发送方把加密后的数据,使用摘要算法生成摘要,把加密数据和该摘要一同发往接收方。接收方使用同样的摘要算法对加密数据进行计算,比照生成的摘要和接受的摘要是否一致,保证了通信数据的完整性。
# 身份认证,真实性
数字签名:发送方要保证通信数据是真实可信的,不是别人伪造的。使用自己的私钥对摘要加密,生成数字签名。数字签名和加密数据被一同发往接收方。
接收方使用发送方的公钥解密,验证签名,拿到摘要,再比对原数据验证完整性。这样就可以像签署文件一样,证明消息确实是发送方发的。
# 编码实现
- 我们使用
Java
编程语言,实现了根据 API 签名认证机制来验证用户请求
。它通过验证用户请求中的签名
,确保请求的合法性和完整性,从而决定是否允许该请求成功调用 API。
# 密钥对生成
这一点在前面提到过:每一个在 MemoryAPI 忆汇廊注册过的用户,系统都会为其发放唯一的密钥,用户可以发起接口调用请求时必须携带密钥
我们给用户分配一个密钥对:accessKey 和 secretKey,即公钥和私钥
private String accessKey = "memory";
private String secretKey = "12345678";
# 签名生成
- 该方法使用 SHA256 摘要算法生成签名。它首先将请求体和私钥拼接在一起,然后使用 Digester 类的 SHA256 实例对拼接后的内容进行摘要计算,最后返回生成的签名:
/**
* 生成签名
*
* @param body 请求体
* @param secretKey 私钥
* @return 签名
*/
public static String getSign(String body, String secretKey) {
Digester md5 = new Digester(DigestAlgorithm.SHA256);
String content = body + "." + secretKey;
// 使用 SHA256 摘要算法生成签名
return md5.digestHex(content);
}
# 签名携带
- 功能:根据请求体 body,构建一个包含签名的头部信息映射,该方法返回一个包含特定键值对的 HashMap。
/**
* 构建一个包含签名的头部信息映射
*
* @param body 请求体
* @return 包含特定键值对的 HashMap
*/
public Map<String, String> getHeaderMap(String body) {
Map<String, String> hashMap = new HashMap<>();
hashMap.put("accessKey", accessKey);
hashMap.put("nonce", RandomUtil.randomNumbers(4));
hashMap.put("body", body);
hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
hashMap.put("sign", getSign(body, secretKey));
return hashMap;
}
- 这个方法的目的是构建一个包含
特定头部字段
的 Map 对象,用于 HTTP 请求头,其中包含一些验证信息
以确保请求的完整性和来源。下面是对这些头部字段的详细解释
:accessKey
: 为用户分配的唯一的密钥,用于验证发送请求的实体是否被授权。nonce
: 是一个随机生成的字符串或数字,用于防止重放攻击。body
: 请求的主要内容。timestamp
: 表示当前时间的 Unix 时间戳,用于验证请求是否在合理的时间内发送。sign
: 这是一个使用私钥对消息进行加密的结果,验证消息来源,确保消息未被篡改。
# 发送请求
- 携带请求数据和签名,通过 POST 请求向指定的 URL 发送请求:
/**
* 复读机
*
* @param user 用户名
* @return 返回用户名
*/
public String getUserByPost(User user) {
String json = JSONUtil.toJsonStr(user);
return HttpRequest.post(GATEWAY_HOST + "/api/name/user")
.addHeaders(getHeaderMap(json))
.body(json)
.execute()
.body();
}
# 签名验证
- 接口提供方收到调用请求,获取数据和签名 sign,用一致的摘要算法生成签名 serverSign,比较两个签名是否一致,即可判断在请求过程中数据有没有被篡改,提升了安全性(以下省略其他请求参数的校验)
@PostMapping("/user")
public String getUserNameByPost(@RequestBody User user, HttpServletRequest request) {
String accessKey = request.getHeader("accessKey");
String nonce = request.getHeader("nonce");
String timestamp = request.getHeader("timestamp");
String body = request.getHeader("body");
String sign = request.getHeader("sign");
................................
String serverSign = SignUtils.getSign(body, secretKey);
if (sign == null || !sign.equals(serverSign)) {
return handleNoAuth(response);
}
return "POST 我的名字是: " + user.getName();
}
- 至此,我们实现了根据
API 签名认证机制
,判断用户请求能否成功地调用相应的接口服务