0


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

文章目录

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

  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 =newUserEntity();
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(newString[keySet.size()]);Arrays.sort(keyArray);StringBuilder sb =newStringBuilder();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 =newAPIRequestEntity();
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())){thrownewException("数据签名错误!");}

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(newString[keySet.size()]);Arrays.sort(keyArray);StringBuilder sb =newStringBuilder();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())){thrownewException("公钥验签错误!");}System.out.println();System.out.println("【提供方】验证通过!");

完整代码补充

packageopenApi;importcom.alibaba.fastjson.JSONObject;importcom.google.common.collect.Maps;importlombok.SneakyThrows;importorg.apache.commons.codec.binary.Hex;importjava.nio.charset.StandardCharsets;importjava.security.*;importjava.security.interfaces.RSAPrivateKey;importjava.security.interfaces.RSAPublicKey;importjava.security.spec.PKCS8EncodedKeySpec;importjava.security.spec.X509EncodedKeySpec;importjava.util.*;publicclassAppUtils{/**
     * key:appId、value:appSecret
     */staticMap<String,String> appMap =Maps.newConcurrentMap();/**
     * 分别保存生成的公私钥对
     * key:appId,value:公私钥对
     */staticMap<String,Map<String,String>> appKeyPair =Maps.newConcurrentMap();publicstaticvoidmain(String[] args)throwsException{// 模拟生成appId、appSecretString appId =initAppInfo();// 根据appId生成公私钥对initKeyPair(appId);// 模拟请求方String requestParam =clientCall();// 模拟提供方验证serverVerify(requestParam);}privatestaticStringinitAppInfo(){// appId、appSecret生成规则,依据之前介绍过的方式,保证全局唯一即可String appId ="123456";String appSecret ="654321";
        appMap.put(appId, appSecret);return appId;}privatestaticvoidserverVerify(String requestParam)throwsException{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())){thrownewException("数据签名错误!");}// 从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(newString[keySet.size()]);Arrays.sort(keyArray);StringBuilder sb =newStringBuilder();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())){thrownewException("公钥验签错误!");}System.out.println();System.out.println("【提供方】验证通过!");}publicstaticStringclientCall(){// 假设接口请求方与接口提供方,已经通过其他渠道,确认了双方交互的appId、appSecretString appId ="123456";String appSecret ="654321";String timestamp =String.valueOf(System.currentTimeMillis());// 应该为随机数,演示随便写一个String nonce ="1234";// 业务请求参数UserEntity userEntity =newUserEntity();
        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(newString[keySet.size()]);Arrays.sort(keyArray);StringBuilder sb =newStringBuilder();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 =newAPIRequestEntity();
        apiRequestEntity.setHeader(header);
        apiRequestEntity.setBody(userEntity);String requestParam =JSONObject.toJSONString(apiRequestEntity);System.out.println("【请求方】接口请求参数: "+ requestParam);return requestParam;}/**
     * 私钥签名
     *
     * @param privateKeyStr
     * @param dataStr
     * @return
     */publicstaticStringsha256withRSASignature(String privateKeyStr,String dataStr){try{byte[] key =Base64.getDecoder().decode(privateKeyStr);byte[] data = dataStr.getBytes();PKCS8EncodedKeySpec keySpec =newPKCS8EncodedKeySpec(key);KeyFactory keyFactory =KeyFactory.getInstance("RSA");PrivateKey privateKey = keyFactory.generatePrivate(keySpec);Signature signature =Signature.getInstance("SHA256withRSA");
            signature.initSign(privateKey);
            signature.update(data);returnnewString(Base64.getEncoder().encode(signature.sign()));}catch(Exception e){thrownewRuntimeException("签名计算出现异常", e);}}/**
     * 公钥验签
     *
     * @param dataStr
     * @param publicKeyStr
     * @param signStr
     * @return
     * @throws Exception
     */publicstaticbooleanrsaVerifySignature(String dataStr,String publicKeyStr,String signStr)throwsException{KeyFactory keyFactory =KeyFactory.getInstance("RSA");X509EncodedKeySpec x509EncodedKeySpec =newX509EncodedKeySpec(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
     */publicstaticvoidinitKeyPair(String appId)throwsException{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",newString(Base64.getEncoder().encode(publicKey.getEncoded())));
        keyMap.put("privateKey",newString(Base64.getEncoder().encode(privateKey.getEncoded())));
        appKeyPair.put(appId, keyMap);}privatestaticStringgetAppSecret(String appId){returnString.valueOf(appMap.get(appId));}@SneakyThrowspublicstaticStringgetSHA256Str(String str){MessageDigest messageDigest;
        messageDigest =MessageDigest.getInstance("SHA-256");byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));returnHex.encodeHexString(hash);}}

总结

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

标签: java 安全

本文转载自: https://blog.csdn.net/CSDN_WYL2016/article/details/124872582
版权归原作者 码拉松 所有, 如有侵权,请联系我们删除。

“如何设计安全可靠的开放接口---之签名(sign)”的评论:

还没有评论