springboot 前后端,基于票据+SHA派生密钥+SM4加解密
背景
目的:1、考试查卷,前后端传递加密的答案;2、保证一定的安全性1)每次查卷生成唯一的、任意票据,作为秘钥生成依据;2)每次请求生成票据,存入redis,并设置过期时间,保证该票据仅1次有效(且有有效期);设计:1)前端请求后端生成票据:票据依据 功能模块:用户id:uuid 生成;2)前端拿着票据,请求答案sm4秘钥生成:后端用票据进行 sha256 摘要计算,并截取固定的的长度作为秘钥后端加密答案:使用 hutool 封装的 bouncycastle 进行sm4 加密3)前端解密答案:前端依据票据:使用 crypto-js (sha256)+ sm-crypto (sm4)进行解密注:sha256 也可以改成hash512 + 依据ts 约定开始截取位数,进一步加大破解难度;
一、后端实现
1 maven依赖
这里测试项目用的:JDK17
<!-- hutool - 每个包都引用 -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.38</version>
</dependency><!-- 国密 -->
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk18on</artifactId><version>1.81</version>
</dependency>
2 工具类
TicketCryptoUtil
package com.common.util;import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.symmetric.SM4;import java.util.Arrays;/*** 票据相关加密工具类* <p>* 说明:* - 使用 SHA-256 摘要对票据进行密钥派生(取前16字节)* - 使用 SM4 算法(16进制编码)进行加密和解密*/
public class TicketCryptoUtil {/*** 使用 SHA-256 对票据进行密钥派生,生成 SM4 加解密密钥(16字节)** @param ticket 票据字符串* @return 16字节 SM4 密钥*/public static byte[] generateSm4KeyFromTicket(String ticket) {byte[] sha256Bytes = DigestUtil.sha256(ticket);return Arrays.copyOf(sha256Bytes, 16);}/*** 使用指定的SM4密钥加密明文** @param plainText 加密内容* @param sm4Key sm4密钥* @return 16禁止加密字符串*/public static String sm4Encrypt(String plainText, byte[] sm4Key) {if (StrUtil.isBlank(plainText)) {return plainText;}SM4 sm4 = SmUtil.sm4(sm4Key);return sm4.encryptHex(plainText);}/*** 使用指定的SM4密钥解密密文(16进制编码密文)*/public static String sm4Decrypt(String hexCipher, byte[] sm4Key) {if (StrUtil.isBlank(hexCipher)) {return hexCipher;}SM4 sm4 = SmUtil.sm4(sm4Key);return sm4.decryptStr(hexCipher);}
}
3 工具类测试:
String str = "你好,中国!";String ticket = "随便的字符串";byte[] sm4Key = TicketCryptoUtil.generateSm4KeyFromTicket(ticket);String encryptHexStr = TicketCryptoUtil.sm4Encrypt(str, sm4Key);log.info("加签后端16进制字符串:" + encryptHexStr);String decryptStr = TicketCryptoUtil.sm4Decrypt(encryptHexStr, sm4Key);log.info("解密后的字符串:" + decryptStr);
测试结果
二、前端实现
1 安装依赖
前端国密的基本使用,见npm仓库:http://www.npmmirror.com/package/sm-crypto
# 包含各种国际加密的实现
pnpm install crypto-js
# 包含国密加密 实现
pnpm install sm-crypto
2 工具类
common.js
import crypto from 'sm-crypto'
import sha256 from 'crypto-js/sha256.js'
import Hex from 'crypto-js/enc-hex.js'// 下载文件
const common = {/*** 根据票据派生16字节(32个hex字符)SM4密钥* @param {string} ticket* @returns {string} 32位hex字符串,作为sm-crypto的key*/generateSm4KeyFromTicket(ticket) {const hash = sha256(ticket).toString(Hex);return hash.substring(0, 32); // 16字节16*2=32hex字符},/*** 使用指定的SM4密钥,加密明文,返回Base64密文* @param {string} plainText 加密内容* @param {string} sm4Key 32位hex字符串* @returns {string} 16进制密文*/sm4Encrypt(plainText, sm4Key) {if (!plainText || typeof plainText !== 'string') {return '';}if (!sm4Key || typeof sm4Key !== 'string' || sm4Key.length !== 32) {console.warn('SM4加密失败:密钥无效', sm4Key);return '';}try {return crypto.sm4.encrypt(plainText, sm4Key);} catch (err) {console.warn('SM4加密异常:', err);return '';}},/*** 使用指定的SM4密钥,解密16 进制密文,返回明文* @param {string} hexCipher 16 进制字符串* @param {string} sm4Key 32位hex字符串* @returns {string}*/sm4Decrypt(hexCipher, sm4Key) {if (!hexCipher || typeof hexCipher !== 'string') {return '';}if (!sm4Key || typeof sm4Key !== 'string' || sm4Key.length !== 32) {console.warn('SM4解密失败:密钥无效', sm4Key);return '';}try {return crypto.sm4.decrypt(hexCipher, sm4Key);} catch (err) {console.warn('SM4解密异常:', err);return '';}}
};export default common
3 前端请求基本实现
这里设计到方法,仅做概述
<template>......
</template>
<script setup>
import common from "@/utils/common.js";// 1 生成查看答案的票据const {ticket} = await ticketApi.getTicket({userId: 用户id,sysFunction: '系统所属的功能模块'})// 2 根据票据,获取解密需要的密钥const sm4Key = common.generateSm4KeyFromTicket(ticket);// 3 请求 试题答案let questionTempList = ......// 4 解密答案try {questionTempList.forEach(question => {question.correctAnswer = common.sm4Decrypt(question.correctAnswer, sm4Key);});// 4.1 响应式变量尽量一次性修改值,减少资源消耗questionList.value = questionTempList;} catch (err) {ElMessage.error(err);}
</script>