简介
本文介绍了基于国密sm2+sm4加密算法的接口鉴权demo
demo是java8 + SpringBoot
demo地址:https://github.com/destinyol/sm2sm4-interface-auth
背景介绍
SM2与SM4作为我国自主研发的商用密码算法,在接口安全领域形成了优势互补的加密体系。其中SM2作为非对称加密算法,基于椭圆曲线密码学(ECC)实现数字签名和密钥协商,具有安全性高、密钥短的特点,但加解密效率较低;SM4作为对称加密算法,采用128位分组加密机制,具备运算速度快、适合大数据量处理的优势,但存在密钥分发安全隐患。
在接口鉴权场景中,采用SM2进行身份认证与密钥协商,结合SM4实现业务数据加密,既能通过非对称加密解决密钥传输难题,又能利用对称加密保障传输效率,符合国密标准的同时有效防范中间人攻击和数据篡改风险。
加密解密流程
加密流程
首先给每一个服务分配一个AppId和AppKey作为服务id和私钥key,每个服务的AppId和AppKey是不可重复,会用于验证数据完整性,防止篡改。然后生成一个64字符长度的随机字符串作为私钥privateKey,由privateKey在圆锥曲线上计算得到公钥坐标字符串(x,y),x和y都是长度为64的字符串,作为公钥publicKey,公钥和私钥只生成一次,作为系统级配置变量,交由服务配置中心托管代理。
其中前两位为标记位,后128位是公钥坐标拼接而成,这样一个130长度字符串就作为SM2算法的加密公钥。在每一次发送请求时,由服务请求者随机生成一个16位随机字符串random16Key,并把这个字符串通过SM2算法和publicKey其中的坐标加密生成keyA。
然后再将服务请求者的AppId、当前时间戳timestamp和keyA拼接起来,借助签名算法和AppKey生成完整性数字签名,用于服务接收者鉴权。
最后将请求的数据部分序列化为Json字符串cipherData,将random16Key作为密钥,使用SM4算法生成加密数据decryptStr。
最终发送请求的请求体就是字符串decryptStr,请求头需要包含以下参数。
参数名称 | 类型 | 是否必须 | 描述 |
---|---|---|---|
AppId | String | 是 | 服务请求者Appld,用于标识请求来源的应用程序。 |
timestamp | String | 是 | 当前时间的时间戳,以字符串格式提供,用于确保请求的时效性。 |
keyA | String | 是 | 经过SM2加密的SM4的16位密钥,用于加密和解密通信过程中的数据。 |
encode | String | 是 | 鉴权字符串,用于验证请求的合法性。 |
解密流程
首先与数据库中服务AppId匹配,若不存在则抛出服务不存在异常。接着用同样的encode生成方法拼接请求头中的参数与服务私钥AppKey,经过数字签名算法生成encode,比对请求中encode与生成的是否一致,若不一致则请求涉嫌伪造篡改数据,抛出异常。最后就可以用本地privateKey解密SM2加密的16位随机字符串random16Key,再使用random16Key解密SM4算法加密的数据decryptStr字符串,拿到最终数据Json字符串,鉴权流程结束。
对于最后请求的返回体,使用与请求方法同样的鉴权逻辑,对数据进行加密,由服务请求者本地解密拿到返回数据。
时序图
示例代码
// 模拟获取服务的appid
public static final String appid = "ed777143e9102d0058c11f7498f4cb0bcvi2rrrdoyojx8by";
// 模拟获取服务的appKey
public static final String privateKey = "eqwetwqyetwqyegqwetqvcsvafsagfsqrwqwgqwhvsnc0";
发送请求端
/**
* /login 发送请求案例
*/
public static void login(){
HashMap<String,String> headerMap = new HashMap<String,String>();
headerMap.put("appid",appid); // appid是服务id
//获取当前时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String nowdate = sdf.format(new Date());
headerMap.put("timestamp",nowdate);
//生成16位随机数
String random = RandomStringGenerator.generateRandomString();
String sm4key = sm2EncryptData_CBC(random);
headerMap.put("keyA",sm4key);
//获取签名
String encode = Signature.getSignature(headerMap, appKey);//appKey是服务私钥
headerMap.put("encode",encode);
headerMap.put("content-type", "application/json");
//请求体
JSONObject jsonObject = new JSONObject();
// 填入数据======================================================
jsonObject.put("password","xxxxxxxxxx111");
jsonObject.put("account","datadatadata");
String data = encryptData_CBC(com.alibaba.fastjson.JSON.toJSONString(jsonObject),random);
//请求结果
String result = HttpClientUtil.doPost(url+"/test/login", data, headerMap);
System.out.println("result:" + result);
JSONObject object = JSON.parseObject(result);
System.out.println("解密result-datas:" + decryptData_CBC((String) object.get("keyA"), (String) object.get("datas")));
}
接收请求端
/**
* 接收请求案例
* @param request
* @param requestData
* @return
*/
@PostMapping("/login")
public RetResponse login(HttpServletRequest request, @RequestBody String requestData) {
RetResponse response = new RetResponse();
int msgCode = 1;
OpenInterfaceParamDto openInterfaceParamDto = new OpenInterfaceParamDto();
openInterfaceParamDto.setSm4key(request.getHeader("keyA"));
openInterfaceParamDto.setData(requestData);
openInterfaceParamDto.setAppid(request.getHeader("appid"));
openInterfaceParamDto.setEncode(request.getHeader("encode"));
openInterfaceParamDto.setTimestamp(request.getHeader("timestamp"));
try {
ObjectMapper objectMapper = new ObjectMapper();
String jsonData = judgeDecryption(openInterfaceParamDto); // 获取到解密后的数据json字符串
System.out.println("请求接收成功,请求解密后数据为 = "+jsonData);
Object data = objectMapper.readValue(jsonData, Object.class); // 装载到实体类中后续处理业务
// ******************************
// 处理业务,业务处理完毕
HashMap<String,String> res = new HashMap<>(); // 业务返回结果
res.put("data","业务处理结果");
res.put("data2","业务处理结果2");
// ******************************
//拼接返回值json字符串
String dataResJson = com.alibaba.fastjson2.JSON.toJSONString(res);
// 加密返回值
OpenInterfaceParamDto openInterfaceResDto = new OpenInterfaceParamDto();
openInterfaceResDto.setData(dataResJson);
openInterfaceResDto = judgeEncryption(openInterfaceResDto);
response.setAny("keyA",openInterfaceResDto.getSm4key());
response.setDatas(openInterfaceResDto.getData());
}catch (JsonProcessingException e){
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
}
response.ret(response, msgCode, "返回结果提示语句");
return response;
}
private String judgeDecryption(OpenInterfaceParamDto openInterfaceParamDto) {
if (StringUtils.isBlank(openInterfaceParamDto.getSm4key())
|| StringUtils.isBlank(openInterfaceParamDto.getTimestamp())
|| StringUtils.isBlank(openInterfaceParamDto.getAppid())
|| StringUtils.isBlank(openInterfaceParamDto.getEncode())){
throw new RuntimeException("参数不能为空");
}
// ****************************
// appid比对是否存在,并查询对应的应用密钥 `appKey`
if (!openInterfaceParamDto.getAppid().equals(appid)){
throw new RuntimeException("appid服务id不存在");
}
String appKey1 = appKey; // 数据库查到的对应appid的appKey
// ****************************
// 拼接encode并用md5签名加密
HashMap<String,String> encodeMap = new HashMap<>();
encodeMap.put("appid",openInterfaceParamDto.getAppid());
encodeMap.put("timestamp",openInterfaceParamDto.getTimestamp());
encodeMap.put("keyA",openInterfaceParamDto.getSm4key());
String checkEncode = Signature.getSignature(encodeMap,appKey1);
// encode 鉴权
if (!checkEncode.equals(openInterfaceParamDto.getEncode())) throw new RuntimeException("请求鉴权失败");
String decryptData_CBC = decryptData_CBC(openInterfaceParamDto.getSm4key(),openInterfaceParamDto.getData());//解密
return decryptData_CBC;
}
private OpenInterfaceParamDto judgeEncryption(OpenInterfaceParamDto openInterfaceParamDto) {
//第一步:生成16位随机数作为sm4的key
String random = RandomStringGenerator.generateRandomString();
//第二步:使用sm4加密数据
String sm4EncryptData = encryptData_CBC(openInterfaceParamDto.getData(),random);
//第三步:使用sm2把sm4的key加密
String sm2e = sm2EncryptData_CBC(random);
openInterfaceParamDto.setSm4key(sm2e);
openInterfaceParamDto.setData(sm4EncryptData);
return openInterfaceParamDto;
}
相关工具包
/**
* sm2 sm4工具
*/
public class SmUtil {
private final static String secretKey = "JKK12H6QXIL0N6GG";
private final static String privateKey = "E1D0EFA0BA6A370652B2E278E19D204E2D21244AD8D18E1EDCF82A9E24EB47AF";
private final static String publicKey = "0487DC37BB8CC8CD4E4A16FFFDE98C854121CCA026B33BAAF0A2171CC23693BC4AD9F9657D9B4098DFB8D730B74336D3B9603D9325714D9FFFF52D10B103B4F9A3";
/**
* @Description: sm4 解密
* @author: hou yan hui
**/
public static String decryptData_CBC(String key, String cipherText) {
SM2 sm2 = getSM2(privateKey, null);
String decryptStr = StrUtil.utf8Str(sm2.decryptFromBcd(key, KeyType.PrivateKey));
System.out.println("sm2解密:" + decryptStr);
SM4 sm4 = new SM4(Mode.CBC, Padding.PKCS5Padding,decryptStr.getBytes(),decryptStr.getBytes());
String sm41 = sm4.decryptStr(cipherText);
System.out.println("sm4解密:" + sm41);
return sm41;
}
public static String sm2EncryptData_CBC(String key) {
//创建sm2 对象
SM2 sm2 = getSM2(null, publicKey);
String encryptStr = sm2.encryptBcd(key, KeyType.PublicKey);
System.out.println("sm2加密:" + encryptStr);
return encryptStr;
}
public static String decryptData_CBC(String cipherText) {
SM4 sm4 = new SM4(Mode.CBC, Padding.PKCS5Padding,secretKey.getBytes(),secretKey.getBytes());
String sm41 = sm4.decryptStr(cipherText);
System.out.println("sm4解密:" + sm41);
return sm41;
}
public static String encryptData_CBC(String cipherText) {
SM4 sm4 = new SM4(Mode.CBC, Padding.PKCS5Padding,secretKey.getBytes(),secretKey.getBytes());
String sm41 = sm4.encryptHex(cipherText);
System.out.println("sm4加密:" + sm41);
return sm41;
}
public static String encryptData_CBC(String cipherText,String sm4key) {
SM4 sm4 = new SM4(Mode.CBC, Padding.PKCS5Padding,sm4key.getBytes(),sm4key.getBytes());
String sm41 = sm4.encryptHex(cipherText);
System.out.println("sm4加密:" + sm41);
return sm41;
}
private static SM2 getSM2(String privateKey, String publicKey) {
ECPrivateKeyParameters ecPrivateKeyParameters = null;
ECPublicKeyParameters ecPublicKeyParameters = null;
if (StringUtils.isNotBlank(privateKey)) {
ecPrivateKeyParameters = BCUtil.toSm2Params(privateKey);
}
if (StringUtils.isNotBlank(publicKey)) {
if (publicKey.length() == 130) {
//这里需要去掉开始第一个字节 第一个字节表示标记
publicKey = publicKey.substring(2);
}
String xhex = publicKey.substring(0, 64);
String yhex = publicKey.substring(64, 128);
ecPublicKeyParameters = BCUtil.toSm2Params(xhex, yhex);
}
//创建sm2 对象
SM2 sm2 = new SM2(ecPrivateKeyParameters, ecPublicKeyParameters);
sm2.usePlainEncoding();
sm2.setMode(SM2Engine.Mode.C1C2C3);
return sm2;
}
}
/**
* 数字签名
*/
public class Signature {
public static String getSignature(HashMap<String,String> textMap,String authkey){
List<Map.Entry<String, String>> list = new ArrayList<Map.Entry<String, String>>(textMap.entrySet());
// 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序)
Collections.sort(list, new Comparator<Map.Entry<String, String>>() {
public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
return (o1.getKey()).toString().compareTo(o2.getKey());
}
});
//拼接
String str = "";
for (Map.Entry<String, String> atr : list) {
str = str + atr.getKey() + "=" + atr.getValue() + "&";
}
str = str.substring(0, str.length() - 1);
String temp = str+authkey;
//MD5
String return_newstr = Md5Utils.MD5(temp);
//转大写
String return_bigstr = return_newstr.toUpperCase();
return return_bigstr;
}
}