sm2+sm4混合加密服务间开放接口鉴权

简介

本文介绍了基于国密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,公钥和私钥只生成一次,作为系统级配置变量,交由服务配置中心托管代理。

img

其中前两位为标记位,后128位是公钥坐标拼接而成,这样一个130长度字符串就作为SM2算法的加密公钥。在每一次发送请求时,由服务请求者随机生成一个16位随机字符串random16Key,并把这个字符串通过SM2算法和publicKey其中的坐标加密生成keyA。

img

然后再将服务请求者的AppId、当前时间戳timestamp和keyA拼接起来,借助签名算法和AppKey生成完整性数字签名,用于服务接收者鉴权。

img

最后将请求的数据部分序列化为Json字符串cipherData,将random16Key作为密钥,使用SM4算法生成加密数据decryptStr。

img

最终发送请求的请求体就是字符串decryptStr,请求头需要包含以下参数。

参数名称类型是否必须描述
AppIdString服务请求者Appld,用于标识请求来源的应用程序。
timestampString当前时间的时间戳,以字符串格式提供,用于确保请求的时效性。
keyAString经过SM2加密的SM4的16位密钥,用于加密和解密通信过程中的数据。
encodeString鉴权字符串,用于验证请求的合法性。
解密流程

首先与数据库中服务AppId匹配,若不存在则抛出服务不存在异常。接着用同样的encode生成方法拼接请求头中的参数与服务私钥AppKey,经过数字签名算法生成encode,比对请求中encode与生成的是否一致,若不一致则请求涉嫌伪造篡改数据,抛出异常。最后就可以用本地privateKey解密SM2加密的16位随机字符串random16Key,再使用random16Key解密SM4算法加密的数据decryptStr字符串,拿到最终数据Json字符串,鉴权流程结束。

对于最后请求的返回体,使用与请求方法同样的鉴权逻辑,对数据进行加密,由服务请求者本地解密拿到返回数据。

时序图

image-20250206152647567

示例代码

// 模拟获取服务的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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值