文章目录

  • 【如何设计安全可靠的开放接口】系列
  • 前言
    • 一、前置知识
    • 二、签名的作用
      • 1. 数据防篡改
      • 2. 身份防冒充
    • 三、流程说明
      • 前置准备
      • 交互流程
        • 接口请求方
        • 接口提供方
        • 完整代码补充
  • 总结

【如何设计安全可靠的开放接口】系列

1. 如何设计安全可靠的开放接口—之Token
2. 如何设计安全可靠的开放接口—之AppId、AppSecret
3. 如何设计安全可靠的开放接口—之签名(sign)
4. 如何设计安全可靠的开放接口【番外篇】—关于MD5应用的介绍
5. 如何设计安全可靠的开放接口—还有哪些安全保护措施
6. 如何设计安全可靠的开放接口—对请求参加密保护
7. 如何设计安全可靠的开放接口【番外篇】— 对称加密算法

前言

本节内容可以说是开放接口设计的关键所在,上一节在最后也提到了appId、appSecret如果没有接下来的这一步签名,将变的毫无意义,所以本节我们就来正式看看应该如何进行签名。

一、前置知识

首先,在介绍签名方式之前,我们必须先了解2个概念,分别是:非对称加密算法(比如:RSA)、摘要算法(比如:MD5)。

简单来说:

  1. 非对称加密的应用场景一般有两种,一种是公钥加密,私钥解密,可以应用在加解密场景中(不过由于非对称加密的效率实在不高,用的比较少),还有一种就是结合摘要算法,把信息经过摘要后,再用私钥加密,公钥用来解密,可以应用在签名场景中,也是我们将要使用到的方式。

大致看看RSASignature签名的方式,稍后用到SHA256withRSA底层就是使用的这个方法。

  1. 摘要算法与非对称算法的最大区别就在于,它是一种不需要密钥的且不可逆的算法,也就是一旦明文数据经过摘要算法计算后,得到的密文数据一定是不可反推回来的。

二、签名的作用

好了,现在我们再来看看签名,签名主要可以用在两个场景,一种是数据防篡改,一种是身份防冒充,实际上刚好可以对应上前面我们介绍的两种算法。

1. 数据防篡改

顾名思义,就是防止数据在网络传输过程中被修改,摘要算法可以保证每次经过摘要算法的原始数据,计算出来的结果都一样,所以一般接口提供方只要用同样的原数据经过同样的摘要算法,然后与接口请求方生成的数据进行比较,如果一致则表示数据没有被篡改过。

2. 身份防冒充

这里身份防冒充,我们就要使用另一种方式,比如SHA256withRSA,其实现原理就是先用数据进行SHA256计算,然后再使用RSA私钥加密,对方解的时候也一样,先用RSA公钥解密,然后再进行SHA256计算,最后看结果是否匹配。

三、流程说明

前置准备

1、正如前面介绍,appId、appSecret当前通过线下的方式,双方约定好,appSecret需要接口请求方自行保密。
2、公私钥可以由接口提供方来生成,同样通过线下的方式,把私钥交给对方,并要求对方需保密。

交互流程

来自百度百科

接口请求方

1、接口请求方,首先把业务参数,进行摘要算法计算,生成一个签名(sign)

// 业务请求参数
UserEntity userEntity = new UserEntity();
userEntity.setUserId("1");
userEntity.setPhone("13912345678");// 使用sha256的方式生成签名
String sign = getSHA256Str(JSONObject.toJSONString(userEntity));
sign=c630885277f9d31cf449697238bfc6b044a78545894c83aad2ff6d0b7d486bc5

2、然后继续拼接header部的参数,可以使用&符合连接,使用set集合完成自然排序,并且过滤参数为空的key,最后使用私钥加签的方式,得到appSign

Map<String, String> data = Maps.newHashMap();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("sign", sign);
data.put("timestamp", timestamp);
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
System.out.println("【请求方】拼接后的参数:" + sb.toString());
System.out.println();
【请求方】拼接后的参数:appId=123456&nonce=1234&sign=c630885277f9d31cf449697238bfc6b044a78545894c83aad2ff6d0b7d486bc5&timestamp=1653057661381&appSecret=654321【请求方】appSign:m/xk0fkDZlHEkbYSpCPdpbriG/EWG9gNZtInoYOu2RtrLMzHNM0iZe1iL4p/+IedAJN2jgG9pS5o5NZH1i55TVoTbZePdCbR9CEJoHq2TZLIiKPeoRgDimAl14V5jHZiMQCXS8RxWT63W8MKFyZQtB7xCtxVD7+IvLGQOAWn7QX+EmfAUvhgjkaVf2YLk9J9LqtyjfTYeloiP901ZsBZo5y9Gs5P73b+JoEcxmGZRv+Fkv3HnHWTQEpl7W6Lrmd0j44/XupwzHxaanRo5k0ALOVSFohdyMtHk3eOYx/bj+GeMKf8PN4J4tsPndnjyu4XUOnh74aaW9oC2DLiIzr4+Q==

3、最后把参数组装,发送给接口提供方

Header header = Header.builder().appId(appId).nonce(nonce).sign(sign).timestamp(timestamp).appSign(appSign).build();
APIRequestEntity apiRequestEntity = new APIRequestEntity();
apiRequestEntity.setHeader(header);
apiRequestEntity.setBody(userEntity);
String requestParam = JSONObject.toJSONString(apiRequestEntity);
System.out.println("【请求方】接口请求参数: " + requestParam);
【请求方】接口请求参数: {"body":{"phone":"13912345678","userId":"1"},"header":{"appId":"123456","appSign":"m/xk0fkDZlHEkbYSpCPdpbriG/EWG9gNZtInoYOu2RtrLMzHNM0iZe1iL4p/+IedAJN2jgG9pS5o5NZH1i55TVoTbZePdCbR9CEJoHq2TZLIiKPeoRgDimAl14V5jHZiMQCXS8RxWT63W8MKFyZQtB7xCtxVD7+IvLGQOAWn7QX+EmfAUvhgjkaVf2YLk9J9LqtyjfTYeloiP901ZsBZo5y9Gs5P73b+JoEcxmGZRv+Fkv3HnHWTQEpl7W6Lrmd0j44/XupwzHxaanRo5k0ALOVSFohdyMtHk3eOYx/bj+GeMKf8PN4J4tsPndnjyu4XUOnh74aaW9oC2DLiIzr4+Q==","nonce":"1234","sign":"c630885277f9d31cf449697238bfc6b044a78545894c83aad2ff6d0b7d486bc5","timestamp":"1653057661381"}}

接口提供方

1、从请求参数中,先获取body的内容,然后签名,完成对参数校验

Header header = apiRequestEntity.getHeader();
UserEntity userEntity = JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()), UserEntity.class);
// 首先,拿到参数后同样进行签名
String sign = getSHA256Str(JSONObject.toJSONString(userEntity));
if (!sign.equals(header.getSign())) {throw new Exception("数据签名错误!");
}

2、从header中获取相关信息,并使用公钥进行验签,完成身份认证

// 从header中获取相关信息,其中appSecret需要自己根据传过来的appId来获取
String appId = header.getAppId();
String appSecret = getAppSecret(appId);
String nonce = header.getNonce();
String timestamp = header.getTimestamp();
// 按照同样的方式生成appSign,然后使用公钥进行验签
Map<String, String> data = Maps.newHashMap();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("sign", sign);
data.put("timestamp", timestamp);
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
if (!rsaVerifySignature(sb.toString(), appKeyPair.get(appId).get("publicKey"), header.getAppSign())) {throw new Exception("公钥验签错误!");
}
System.out.println();
System.out.println("【提供方】验证通过!");

完整代码补充

package openApi;import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Hex;import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;public class AppUtils {/*** key:appId、value:appSecret*/static Map<String, String> appMap = Maps.newConcurrentMap();/*** 分别保存生成的公私钥对* key:appId,value:公私钥对*/static Map<String, Map<String, String>> appKeyPair = Maps.newConcurrentMap();public static void main(String[] args) throws Exception {// 模拟生成appId、appSecretString appId = initAppInfo();// 根据appId生成公私钥对initKeyPair(appId);// 模拟请求方String requestParam = clientCall();// 模拟提供方验证serverVerify(requestParam);}private static String initAppInfo() {// appId、appSecret生成规则,依据之前介绍过的方式,保证全局唯一即可String appId = "123456";String appSecret = "654321";appMap.put(appId, appSecret);return appId;}private static void serverVerify(String requestParam) throws Exception {APIRequestEntity apiRequestEntity = JSONObject.parseObject(requestParam, APIRequestEntity.class);Header header = apiRequestEntity.getHeader();UserEntity userEntity = JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()), UserEntity.class);// 首先,拿到参数后同样进行签名String sign = getSHA256Str(JSONObject.toJSONString(userEntity));if (!sign.equals(header.getSign())) {throw new Exception("数据签名错误!");}// 从header中获取相关信息,其中appSecret需要自己根据传过来的appId来获取String appId = header.getAppId();String appSecret = getAppSecret(appId);String nonce = header.getNonce();String timestamp = header.getTimestamp();// 按照同样的方式生成appSign,然后使用公钥进行验签Map<String, String> data = Maps.newHashMap();data.put("appId", appId);data.put("nonce", nonce);data.put("sign", sign);data.put("timestamp", timestamp);Set<String> keySet = data.keySet();String[] keyArray = keySet.toArray(new String[keySet.size()]);Arrays.sort(keyArray);StringBuilder sb = new StringBuilder();for (String k : keyArray) {if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名sb.append(k).append("=").append(data.get(k).trim()).append("&");}sb.append("appSecret=").append(appSecret);if (!rsaVerifySignature(sb.toString(), appKeyPair.get(appId).get("publicKey"), header.getAppSign())) {throw new Exception("公钥验签错误!");}System.out.println();System.out.println("【提供方】验证通过!");}public static String clientCall() {// 假设接口请求方与接口提供方,已经通过其他渠道,确认了双方交互的appId、appSecretString appId = "123456";String appSecret = "654321";String timestamp = String.valueOf(System.currentTimeMillis());// 应该为随机数,演示随便写一个String nonce = "1234";// 业务请求参数UserEntity userEntity = new UserEntity();userEntity.setUserId("1");userEntity.setPhone("13912345678");// 使用sha256的方式生成签名String sign = getSHA256Str(JSONObject.toJSONString(userEntity));Map<String, String> data = Maps.newHashMap();data.put("appId", appId);data.put("nonce", nonce);data.put("sign", sign);data.put("timestamp", timestamp);Set<String> keySet = data.keySet();String[] keyArray = keySet.toArray(new String[keySet.size()]);Arrays.sort(keyArray);StringBuilder sb = new StringBuilder();for (String k : keyArray) {if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名sb.append(k).append("=").append(data.get(k).trim()).append("&");}sb.append("appSecret=").append(appSecret);System.out.println("【请求方】拼接后的参数:" + sb.toString());System.out.println();// 使用sha256withRSA的方式对header中的内容加签String appSign = sha256withRSASignature(appKeyPair.get(appId).get("privateKey"), sb.toString());System.out.println("【请求方】appSign:" + appSign);System.out.println();// 请求参数组装Header header = Header.builder().appId(appId).nonce(nonce).sign(sign).timestamp(timestamp).appSign(appSign).build();APIRequestEntity apiRequestEntity = new APIRequestEntity();apiRequestEntity.setHeader(header);apiRequestEntity.setBody(userEntity);String requestParam = JSONObject.toJSONString(apiRequestEntity);System.out.println("【请求方】接口请求参数: " + requestParam);return requestParam;}/*** 私钥签名** @param privateKeyStr* @param dataStr* @return*/public static String sha256withRSASignature(String privateKeyStr, String dataStr) {try {byte[] key = Base64.getDecoder().decode(privateKeyStr);byte[] data = dataStr.getBytes();PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key);KeyFactory keyFactory = KeyFactory.getInstance("RSA");PrivateKey privateKey = keyFactory.generatePrivate(keySpec);Signature signature = Signature.getInstance("SHA256withRSA");signature.initSign(privateKey);signature.update(data);return new String(Base64.getEncoder().encode(signature.sign()));} catch (Exception e) {throw new RuntimeException("签名计算出现异常", e);}}/*** 公钥验签** @param dataStr* @param publicKeyStr* @param signStr* @return* @throws Exception*/public static boolean rsaVerifySignature(String dataStr, String publicKeyStr, String signStr) throws Exception {KeyFactory keyFactory = KeyFactory.getInstance("RSA");X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr));PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);Signature signature = Signature.getInstance("SHA256withRSA");signature.initVerify(publicKey);signature.update(dataStr.getBytes());return signature.verify(Base64.getDecoder().decode(signStr));}/*** 生成公私钥对** @throws Exception*/public static void initKeyPair(String appId) throws Exception {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);KeyPair keyPair = keyPairGenerator.generateKeyPair();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();Map<String, String> keyMap = Maps.newHashMap();keyMap.put("publicKey", new String(Base64.getEncoder().encode(publicKey.getEncoded())));keyMap.put("privateKey", new String(Base64.getEncoder().encode(privateKey.getEncoded())));appKeyPair.put(appId, keyMap);}private static String getAppSecret(String appId) {return String.valueOf(appMap.get(appId));}@SneakyThrowspublic static String getSHA256Str(String str) {MessageDigest messageDigest;messageDigest = MessageDigest.getInstance("SHA-256");byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));return Hex.encodeHexString(hash);}}

总结

到目前为止,可以说我们已经完成了一个开放接口的基本设计,通过签名、加密等方式保证了请求过程中参数不会被修改,以及身份不会被冒充,看似已经非常不错了,然而实际上并非如此,你会发现关于timestamp、nonce这样的参数,好像对我们并没有什么用,此外,如果业务参数不想明文传输,也需要加密改怎么办、以及黑白名单机制等等,让我们留在下一节中继续介绍吧!

如何设计安全可靠的开放接口---之签名(sign)相关推荐

  1. 如何设计安全可靠的开放接口---之Token

    文章目录 [如何设计安全可靠的开放接口]系列 前言 一.Token机制 1. Token生成 2. Session存在的问题 3. JWT是如何解决Session存在的问题的 二.JWT中的数据结构 ...

  2. 如何设计安全可靠的开放接口---之AppId、AppSecret

    文章目录 [如何设计安全可靠的开放接口]系列 前言 AppId的使用 AppId的生成 AppSecret生成 总结 [如何设计安全可靠的开放接口]系列 1. 如何设计安全可靠的开放接口-之Token ...

  3. 如何设计安全可靠的开放接口---对请求参加密保护

    文章目录 [如何设计安全可靠的开放接口]系列 前言 AES加解密 代码实现 [如何设计安全可靠的开放接口]系列 1. 如何设计安全可靠的开放接口-之Token 2. 如何设计安全可靠的开放接口-之Ap ...

  4. ​如何设计一个安全可靠的 API 接口?

    作者 | 阿文 责编 | 屠敏 出品 | CSDN(ID:CSDNnews) 最近几年,随着RESTful API开始风靡,使用HTTP header来传递认证令牌似乎变得理所应当,通过 RESTfu ...

  5. App开放接口api安全:Token签名sign的设计与实现

    点击上方蓝色"方志朋",选择"设为星标"回复"666"获取独家整理的学习资料! 来源:cnblogs.com/whcghost/p/5657 ...

  6. python api接口 安全_App开放接口api安全性的设计与实现

    前言 在app开放接口api的设计中,避免不了的就是安全性问题,因为大多数接口涉及到用户的个人信息以及一些敏感的数据,所以对这些接口需要进行身份的认证, 那么这就需要用户提供一些信息,比如用户名密码等 ...

  7. 魔高一丈道高一尺,开放接口安全性设计

    2019独角兽企业重金招聘Python工程师标准>>> 乐猿社区,程序员的花果山 问题 在开放接口api的设计中,避免不了的就是安全性问题,因为大多数接口涉及到用户的个人信息以及一些 ...

  8. 开放接口/RESTful/Api服务的设计和安全方案详解

    一.总体思路 这个涉及到两个方面问题: 一个是接口访问认证问题,主要解决谁可以使用接口(用户登录验证.来路验证) 一个是数据数据传输安全,主要解决接口数据被监听(HTTPS安全传输.敏感内容加密.数字 ...

  9. API 开放接口设计之 appId,appSecret,accessToken (同微信开发平台接口)

    前篇:如何设计开放 Api 以下链接来源于网络素材: 需要考虑点摘录一: https://blog.csdn.net/weixin_34414196/article/details/92105613 ...

最新文章

  1. soj1201- 约数
  2. Go 语言编程 — go-restful RESTful 框架
  3. 思科模拟器32位_三款另类的68k Mac模拟器
  4. android unzip file,Unzip File in Android Assets
  5. 程序4-1 对每个命令行参数打印文件类型
  6. mysql与php的连接_PHP与Mysql的连接
  7. div+css实现背景透明
  8. Blender:雕刻笔刷动态图解(一)
  9. win7 计算器 android,win7计算器
  10. 设计模式之 策略模式
  11. 生成自己的浏览器证书
  12. 01-Weakly supervised consistency
  13. Spring Boot 学习笔记 8 : Elasticsearch
  14. Java 一个数字、字母、汉字各占几个字节
  15. 看甲骨文如何在云端一路高歌猛进!
  16. cad图片边框怎么去掉
  17. 已知空间一点和法向量,如何计算空间平面方程
  18. Web前端大作业制作个人网页(html+css+javascript)
  19. 火车票软件哪个好用_订车票的软件哪个好?2018十大购买车票APP排行榜推荐
  20. python抓取抖音评论_一篇文章教会你用Python抓取抖音app热点数据

热门文章

  1. 关于图片防盗链 - 图片加载报错403,但可以单独打开图片链接
  2. CSS3 连续向下循环播放动画
  3. Word出现灰色的很多小点点、箭头或方框的解决方法~
  4. 动态规划背包问题之多重背包详解
  5. uniapp基于Android的电脑配件商城组装机配置系统
  6. UML概述及UML类图详解
  7. 顺其自然 - 为所当为
  8. iOS项目开发实战——实现苹果本地消息通知推送服务
  9. 添加视频的html语言,添加视频内容.html
  10. CSS过渡、变形及动画