1.Http接口安全概述:
1.1、Http接口是互联网各系统之间对接的重要方式之一,使用http接口,开发和调用都很方便,也是被大量采用的方式,它可以让不同系统之间实现数据的交换和共享,但由于http接口开放在互联网上,那么我们就需要有一定的安全措施来保证不能是随随便便就可以调用;
1.2、目前国内互联网公司主要采用两种做法实现接口的安全:
一种是以支付宝等支付公司为代表的私钥公钥签名验证机制;
一种是大量互联网企业都常采用的参数签名验证机制;
2. Http接口安全演进:
2.1.完全开放的接口(完全开放)
2.2.接口参数签名(基本安全)
2.3.接口参数签名+时效性验证(更加安全)
2.4.接口参数私钥签名公钥验签(固若金汤)
2.5.接口参数签名+Https(金钟罩)
2.6.口参数私钥签名公钥验签+Https(金钟罩)
总之:安全是相对的,只有相对的安全,没有绝对的安全!
3.Http接口安全设计及应用
3.1 接口参数私钥签名公钥验签
先来看看私钥+公钥的安全模式,这是一种更为安全的方式,它通过私钥和公钥实现接口的安全,目前互联网中主要是以支付宝
为代表的公司采用这种机制;(有很多开放平台也是采用这种机制) . 具体业务流所示:
该签名是通过4个秘钥来实现的,分别是:
客户端应用私钥 , 客户端公钥 , 服务端应用私钥 , 服务端公钥.
私钥都是用来生成签名的,公钥都是用来解密的,客户端的公钥解密客户端的私钥生成的签名,服务端的公钥解密服务端的私钥生
成的签名,相信这样解释应该会比较好理解的.
好了,下面就来看看是如何具体操作的,我的具体操作步骤:
首先,4把密钥都是通过OpenSSL工具生成,你需要先获得这个工具:官方网站:https://www.openssl.org/
介绍一下这个工具吧,OpenSSL 是一个开源的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及SSL协议,并提供丰富的应用程序测试或其它目的使用;OpenSSL整个软件包大概可以分成三个主要的功能部分:SSL协议库、应用程序以及密码算法库;
3.1.1确保Linux已经安装openssl :
使用命令检查Linux是否已经安装openssl:yum list installed | grep openssl
如果没有安装,则执行命令进行安装:yum install openssl openssl-devel -y
3.1.2创建秘钥生成的目的地(文件夹):
ps : 我是在根目录中的soft文件夹下创建的
mkdir server , mkdir client 先创建这两个文件,然后Linux会默认将生成的秘钥放入其中.
3.1.3 生成秘钥
进入server文件,输入openssl 进入Openssl命令行;
使用openssl生成私钥,执行如下命令:
genrsa -out rsa_private_key.pem 2048
注意一点Java开发者需要将私钥转换成PKCS8格式,其他语言不用这一步操作,执行如下命令:
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out rsa_private_key_pkcs8.pem
使用openssl生成公钥,执行如下命令:
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
退出openssl命令行:exit
经过以上步骤,我们可以在当前目录中(server)看到三个文件:
rsa_private_key.pem(RSA私钥)
rsa_private_key_pkcs8.pem(pkcs8格式RSA私钥)(我们java要使用的私钥是这一个)
rsa_public_key.pem(对应RSA公钥)
client端的秘钥与server端的秘钥生成一样,这里就不叙述了,想体验的朋友自己按照上一步操作再做一遍即可.
3.2 Demo
好了,现在就可以使用生成的秘钥来做一个简单的Demo来检验一下了.
首先这里提供一个签名处理工具类:
说明:该工具类依赖的包是java.security,该包JDK8才有,所以,如果使用下面这个Demo的话,建议检查自己的JDK是不是JDK8,如果是JDK8以下的版本,以下的Demo是用不了的.解决方法就是,使用第三方提供的依赖包,效果是相同的,依赖包如下:
以下是签名工具类:
package com.kinglong.http.utils;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* 签名处理工具类
*
* @author haojinlong
*
*/
public class MyRSAUtils {
public static final String CHARSET = "utf-8";
/**
* RSA私钥签名
*
* @param src 客户端传过来的原始参数
* @param priKey 我们的客户端私钥
* @return
* @throws Exception
*/
public static String sign (String src, String priKey) {
try {
KeyFactory fac = KeyFactory.getInstance("RSA");
byte[] pribyte = Base64.getDecoder().decode(priKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pribyte);
RSAPrivateKey privateKey = (RSAPrivateKey) fac.generatePrivate(keySpec);
Signature sigEng = Signature.getInstance("SHA1withRSA");
sigEng.initSign(privateKey);
sigEng.update(src.getBytes(MyRSAUtils.CHARSET));
byte[] signature = sigEng.sign();
return Base64.getEncoder().encodeToString(signature);
} catch (Exception e) {
e.printStackTrace();
}
return ;
}
/**
* RSA公钥验证签名
*
* @param src 客户端穿过来的原始数据
* @param sign 签名
* @param publicKey 我们的客户端公钥
* @return
*/
public static boolean signVerify (String sign, String src, String publicKey) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
//将公钥变为一个字节数组
byte[] encodedKey = Base64.getDecoder().decode(publicKey);
//使用秘钥工厂生成一个公钥对象pubKey
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
//使用"SHA1WithRSA"算法,生成签名对象signature
Signature signature = Signature.getInstance("SHA1WithRSA");
signature.initVerify(pubKey);
signature.update(src.getBytes(MyRSAUtils.CHARSET));
boolean bverify = signature.verify(Base64.getDecoder().decode(sign));
return bverify;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
下面是签名时需要的辅助工具类:
package com.kinglong.http.HttpUtils;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
*生成签名需要的辅助工具类
*/
public class SignUtils {
/**
*将传进来的无序参数,转换为有序的字符串输出
*/
public static String generateSortSign(Map
Map
Set
Iterator
StringBuffer stringBuffer = new StringBuffer();
while (iterator.hasNext()){
Map.Entry
String keys = entry.getKey();
String value = (String) entry.getValue();
stringBuffer.append(keys).append(value);
}
return stringBuffer.toString();
}
}
客户端代码:
package com.kinglong.http.controller;
import com.alibaba.fastjson.JSONObject;
import com.kinglong.http.HttpUtils.HttpClientUtils;
import com.kinglong.http.HttpUtils.SignUtils;
import com.kinglong.http.constans.Constans;
import com.kinglong.http.utils.MyRSAUtils;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class HttpClient {
public static void main(String[] args) {
demo();
}
public static void demo(){
String url ="http://localhost:8080/api/verifydemo";
//这里仅做演示使用,所以随手写了几个信息
String realName = "我是验证信息";
String phone = "17000000000";
String idCard = "6403021992120511111";
String bankCard = "5555555555555555555555";
//封装参数
Map
paramMap.put("realName",realName);
paramMap.put("phone",phone);
paramMap.put("idCard",idCard);
paramMap.put("bankCard",bankCard);
//生成客户端签名,注意看,客户端这里生成签名的时候是使用的客户端私钥Constans.CLIENT_PRIVATE_KEY
String sign = MyRSAUtils.sign(SignUtils.generateSortSign(paramMap),Constans.CLIENT_PRIVATE_KEY);
paramMap.put("sign",sign);
//发送请求时注意:因为肯定会不可避免的要输出中文字符,所以记得在使用HttpClient通信的时候,设置编码格式为utf-8
//否则,我想你的签名验证成功的概率基本等于让两条平行线相交成功的概率
String json = HttpClientUtils.doPostByEncode(url,paramMap,"utf-8");
//解析json字符串
JSONObject jsonObject = JSONObject.parseObject(json);
String code = jsonObject.getString("code");
String erroMessage = jsonObject.getString("erroMessage");
JSONObject object = jsonObject.getJSONObject("object");
String result = object.getString("result");
String ret_sign = object.getString("ret_sign");//服务端发回的签名
String resultDesc = object.getString("resultDesc");
//封装参数准备进行验证该返回信息是否是由服务端发回的
//验证时的参数可以根据自己公司的规范去选择,我这里就选了这两个参数,因为偷懒没有生成新的map,所以这里需要clear一下.
paramMap.clear();
paramMap.put("result",result);
paramMap.put("resultDesc",resultDesc);
//调用签名验证工具类进行签名验证
//重点!!!敲黑板啦!!注意看,这里使用的是服务端的公钥来解密服务端发送的签名的,很多人肯定使用了客户端公钥来解密,必然是失败的
//同理,在服务端要是用客户端的公钥解密客户端发送的签名,这个前面的流程图我觉得已经说得很明确了
boolean isTrue = MyRSAUtils.signVerify(ret_sign,SignUtils.generateSortSign(paramMap),Constans.SERVER_PUBLIC_KEY);
if (isTrue){
System.out.println("返回正确信息,可以进行下一步操作");
}else {
System.out.println("返回错误信息,不可以进行下一步操作");
}
}
}
下面是客户端使用的公共常量类:
package com.kinglong.http.constans;
public class Constans {
//客户端私钥
public static final String CLIENT_PRIVATE_KEY="MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClEAp0NDHtb9w5iJfyNOh6DeCRv0RjGFA1CIQ6ZxpfIc65h03hUGsDjcZtWQQZf7d30hiVCcQLylJYJidHQbPcDWULRhObgUxFIFQ37UW8c8DHDiHPNVRjH47ePi9sZIYbVuaWOJkS9NAoSDTRDA1vS7ewWosA9WyUSEuWSEm6eQQV02nlhf/cIsu3biDmq8Y9ffg9lLgEYbH4VQu6bRXgqpy90OxFh3Jh16nbAZXqAJMKCZyJYo5B9ZN4No8Q/EMZe98DNFybOod7WTuQmrS5FM0vCsjQpczBwn+dny5grWl4YgUYgAWOnvPr2iMXVXLqbQjcVEMFKwr/71K9yVehAgMBAAECggEBAJi6nvGm2gu41SznFrEmA3XsIT66m6yVcqGfn7nqbJxZy84fRBCXOG2xYUkMdJ6jbj+QRu6geqXuLwMhSnbEdIfIXRZxYPMiUFAl+cdF5KDa+iU1DlOMJOkS6j75iyfgW7YwUmvtMrY3j+O17CkB3ex9QxoKrVPVwwHxYv9LI+1FT0Fd3157gIbkBTdXnKUc4O4Z4/FTcvPYNR2h9O79xQbcx0clUbj61yQxkxVyN25plxxAVoUW7mKNleH6nFAkeb/gYxLdwlUHm53cYowSDtbzo4udB6qPWj/PvCVgu7UatnEhcyX9ZKCBmrX3+EvWTw9A9dTDSMu4D9CX2QsUq4ECgYEA2pTWhw6ulzWlORu12WcUxDzCVuKV7dGXCQ2MRnGELLm7HzAmRIjd0LubGWAKwUNa6NhWVNsupI8/hDh3hyjGAiTVn0gowDVQj4s/vIx5aYMsg6nxzUl3KrjBJyORHnY8+fFp5V1SHr0G6jOTynsqX0ONMpNZ/zyxiOQG5vGs7A8CgYEAwVHJAe0Eqz2jwLHA10Afsh3vS2Otcjj1cj9jxz1ZFoYIbD64Mc77bem+9x7xpI4w6/WezUOO4gZ8RaAh7PpyMWbTwzq0QHS7bSzKMfpRALyVf7HyIS/QuTUA0oLZXx9vk4wAe0rZBNFuuEtyU3XgHz8OSY/uHeTXJWYtJHTIkU8CgYAD6YgRcMTVNgOYCxPtKTgo7wF3dqTCVe8DHXf2Rs/b0RM1UrJMpbp6ovD6ukpW/TKiWkTpTeb+0QWNA0m4ZJVusmQUbsEz94BSoWZppIYDynJAhQkr6HW2kQn7/ln5lpouyxBfJ5VxsWZvSK8Lf7rZa6caUaLZu6dd0N8CwS6cJwKBgD74B9RTwtiQXF1wyNKUNX7MF1zkG+P/v5s2IKcOSY13nRi9GTxIIke8ApL2BlnGYxMIz3Am2EyxNhtrvIE3VqjWyJVn8ryoCUDXfQjocygdRUjxyl+a9o7NP/ZR3sIIOEzEJogCakwSd9EZ6iRbWeRzopC9jB86ogWxkXS1gXsrAoGAEOvVsv8lpY+TCs3Q78pnedwFIXXkrOknrXq2gb+WHmSSDCELOaqWsX73rdbW4IcmJ6kT2d5bn6hH+/2Zw/+Xpy6maBeLxxscXBfLHwT/85YG5z21LuyikB7ht/tZCNxQOjEfvo5MdCwNN6GhpPSVwNjLAvtiImJYnlDuGPaG+RU=";
//服务端公钥
public static final String SERVER_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqlG1v814kyQIEpLQnyxo/4RUku8PE+csGzH073XoW8xPdXAH8C7Y3isyDKSClTEGeS/SaqioYPFI+YlD0Eag1DEoVPkDcJeWsPv4FZr2ZO2wCenwqUrH5BM7ZapSBLenkgdhTF3SIUJMrTfJPaRJM1wb75SPawHO1zueM0cvcbeGNJOyCG69XPhour0Cei/HcflY3bXVx9kKH8vvAmbosAOrIwwdGSnV1YqSlApBmoobGYcaFPv5Clntghq+6P1ut2RSOrKinr0Q4wc4kIpnSY+j2oQ20OZ7rZXuLyGmMSsglvGwgTIoxHqkXpWP3qGFmYHSQgGCMFWxlT1t3e1AawIDAQAB";
}
ok,客户端就齐活了,下面是服务端:
服务端使用的是springboot框架,以下是pom文件
说明:服务端的签名生成类,签名生成的辅助类都与客户端相同,所以,这里就不写了.
服务端的公共常量类:
package com.kinglong.http.constans;
public class Constans {
//服务端私钥
public static final String SERVER_PRIVATE_KEY="MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCqUbW/zXiTJAgSktCfLGj/hFSS7w8T5ywbMfTvdehbzE91cAfwLtjeKzIMpIKVMQZ5L9JqqKhg8Uj5iUPQRqDUMShU+QNwl5aw+/gVmvZk7bAJ6fCpSsfkEztlqlIEt6eSB2FMXdIhQkytN8k9pEkzXBvvlI9rAc7XO54zRy9xt4Y0k7IIbr1c+Gi6vQJ6L8dx+VjdtdXH2Qofy+8CZuiwA6sjDB0ZKdXVipKUCkGaihsZhxoU+/kKWe2CGr7o/W63ZFI6sqKevRDjBziQimdJj6PahDbQ5nutle4vIaYxKyCW8bCBMijEeqRelY/eoYWZgdJCAYIwVbGVPW3d7UBrAgMBAAECggEBAJtfkxf4T4iblCmteVfb4aVHiQfJwc18VEYy2qkgvOoRhmMx4mv/sKNscGoMIXwMj0U6lQ/r8D8PnmzWBeEYrVslxQ9PYw3xm+y0z+qVxTTpiHBi08L8j0HHMaZbLBtVly6mQOKzrB/fJafXfmQXXRfXbTywH+2UZqb+oiFRTTzEnFMyku5HquA27Mp+K4KNFTVaKiCSadwz+XyFOf1cmUn4oRlYnhgbMKgn2JSyQLfJ5SSgYhnxat1Qbg1HDhOzo6L/NQwCkTzo3B52X56EQXZkk2p1pVRZiwaP/3FCHEenOv9jy+ZffdUFECRCv0Aw2isWqZ4zuVrgWCI801W/AVECgYEA0gwkMSHWlJaGtWhYifgWcOSr/v9l/SAiD0VN57j4THWIAq30WtOVE/ta4XhbSasxcHJIqNckq2inEm2b3jwcS7YKzlfKGl9xw8uL3yVM1YXtKwGrJr1xk8pcWOVBmFaAd8BaOHwsMkE8EhgiwZSctTbmRNeiOz6lIOj2aDu6fE0CgYEAz5SPKeNay00LIGk4Os+Zev1JPhwW4tfwTJxXV97TjvgYoRwUwV6XNjiXoxQD5iOXieK7Cy0GDMzJWxXpmEpI5BVv2X6GMkkH5iXz4rGtzsMNWZ5lF3d2PpRLvRIrSi3btevTohN7UkogyjEPhuEnV47Emsev36lB+bcJwgLBK5cCgYAuEaemdwt/T3yAMUCqEhWp8R2gMhgGapPN0Z+CoVkkO+r223xqp1ldJpYKOcGb6MZRKV+yWG2cgrmSGyRCm+CA4o6AL1UOb7yd+vjUmnO9qUAZXKZTOt28Unfqr22xoddPbIrdNK7k3tX0CgMlfhjYzg+3LaxRXi4Nh8rzlZYTSQKBgBahgq41bEun3aOt9QRsZ7ZB8P9FfrVCh59CmD8rOvNmVwERl62xS1kM+HM+FmK71KSixHOmd/djSDyW+f2xc5ryP1x979GBpsvPrXQ0nNdi6oyvuSPC0XBnKI63cWLH9yExUcRkzVgeXs7MZH33BBwGo6agSKtgv6Gi8/xj4n2HAoGBALD55ZzDDrB17ZOQSuEnn45R3FA62BGipCM8HFdZySeEOf9IPoOxy2xmzQtMc2VESRQeviw92E5gxNxrRgIHSQvPJryaHOjoMKI9+kDBejZvBEHWi23E4tobPUOUH/jBJJQ7Ue5aRB6IS/jlMzmVhan8vefnEXRDT5EnSfxV3Pa6";
//客户端公钥
public static final String CLIENT_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApRAKdDQx7W/cOYiX8jToeg3gkb9EYxhQNQiEOmcaXyHOuYdN4VBrA43GbVkEGX+3d9IYlQnEC8pSWCYnR0Gz3A1lC0YTm4FMRSBUN+1FvHPAxw4hzzVUYx+O3j4vbGSGG1bmljiZEvTQKEg00QwNb0u3sFqLAPVslEhLlkhJunkEFdNp5YX/3CLLt24g5qvGPX34PZS4BGGx+FULum0V4KqcvdDsRYdyYdep2wGV6gCTCgmciWKOQfWTeDaPEPxDGXvfAzRcmzqHe1k7kJq0uRTNLwrI0KXMwcJ/nZ8uYK1peGIFGIAFjp7z69ojF1Vy6m0I3FRDBSsK/+9SvclXoQIDAQAB";
}
服务端接口:
package com.kinglong.http.controller;
import com.kinglong.http.constans.Constans;
import com.kinglong.http.rto.ResponseObject;
import com.kinglong.http.utils.MyRSAUtils;
import com.kinglong.http.utils.SignUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Controller
public class HttpController {
@RequestMapping("/api/verifydemo")
@ResponseBody
public Object demo(@RequestParam(value = "realName")String realName,
@RequestParam(value = "phone")String phone,
@RequestParam(value = "idCard")String idCard,
@RequestParam(value = "bankCard")String bankCard,
@RequestParam(value = "sign")String sign){
ResponseObject responseObject = new ResponseObject();
Map
//签名参数验证
if (StringUtils.isEmpty(realName)){
responseObject.setErroMessage("真实姓名不能为空");
responseObject.setCode("0000");
}else if (StringUtils.isEmpty(phone)){
responseObject.setErroMessage("手机号不能为空");
responseObject.setCode("0000");
}else if (StringUtils.isEmpty(idCard)){
responseObject.setErroMessage("身份证不能为空");
responseObject.setCode("0000");
}else if (StringUtils.isEmpty(bankCard)){
responseObject.setErroMessage("银行卡不能为空");
responseObject.setCode("0000");
}else if (StringUtils.isEmpty(sign)){
responseObject.setErroMessage("签名不能为空");
responseObject.setCode("0000");
}else {
//封装参数进行验证
map.put("realName",realName);
map.put("phone",phone);
map.put("idCard",idCard);
map.put("bankCard",bankCard);
//注意看,这里使用的就是客户端的公钥进行签名的解密
boolean isTrue = MyRSAUtils.signVerify(sign,SignUtils.generateSortSign(map),Constans.CLIENT_PUBLIC_KEY);
if (!isTrue){
responseObject.setErroMessage("签名有误,请核查后再次尝试");
responseObject.setCode("0000");
}
}
//0000是失败,1111是成功
String code = responseObject.getCode();
if(code!= && code.equals("0000")){
map.clear();
map.put("result","0000");
map.put("resultDesc",responseObject.getErroMessage());
//使用服务端私钥生成返回的签名
String ret_sign = MyRSAUtils.sign(SignUtils.generateSortSign(map),Constans.SERVER_PRIVATE_KEY);
map.put("ret_sign",ret_sign);
responseObject.setObject(map);
}else {
map.clear();
map.put("result","ok");
map.put("resultDesc","验证通过");
//使用服务端私钥生成返回的签名
String ret_sign = MyRSAUtils.sign(SignUtils.generateSortSign(map),Constans.SERVER_PRIVATE_KEY);
map.put("ret_sign",ret_sign);
responseObject.setObject(map);
responseObject.setCode("1111");
responseObject.setErroMessage("验证成功");
}
return responseObject;
}
}
over,以上就是使用私钥+公钥进行http接口安全设计的简单上手示例.
个人觉得,能把这个私钥公钥的搞明白了,其他的几个接口安全设计也就不在话下了,基本看一下流程图就能了然于心了.这里就放出参数安全设计的流程图,具体代码就不写了,有兴趣的可以尝试着做一下.
为了方便练习的盆友快速理解,那就顺带提几个需要注意的点吧.
在做:"接口参数签名+时效性验证(更加安全)"时,注意服务端返回的签名就不需要再传时间戳了,理应是要传的,但是一般很少有人这么做.不过,要不要这么做,也得看公司的要求嘛.
在拼装秘钥的时候,注意字符串首先得进行排序,不管是升序还是降序,亦或是其他的顺序(比如公司要求的顺序),如果不先按照一个约定的顺序进行排序,那么势必会造成客户端与服务端参数的字符串排列顺序不同,致使无法验证成功.这里就提供一个简便的排序工具,并且使用MD5进行16进制加密
---------------------
作者:haokinglong_java
来源:CSDN
原文:https://blog.csdn.net/hjl021/article/details/79286830
版权声明:本文为博主原创文章,转载请附上博文链接!