等保3加密
一、什么是等级保护?
网络安全等级保护是指对国家重要信息、法人和其他组织及公民的专有信息以及公开信息和存储、传输、处理这些信息的信息系统分等级实行安全保护。对信息系统中使用的信息安全产品实行按等级管理,对信息系统中发生的信息安全事件分等级响应、处置。
简单来说,就是国家制定的一套网络安全考试大纲,根据信息系统的重要性和被破坏后可能带来的危害程度,将其划分为不同的安全保护等级,并对应不同的保护要求。
二、等级保护的五个级别
根据2019年发布的《信息安全技术网络安全等级保护基本要求》(GB/T22239-2019),等保分为五个级别:
第一级(自主保护级):适用于一般系统,系统遭到破坏后,会对公民、法人和其他组织的合法权益造成损害。本级需要用户自主进行保护。
第二级(指导保护级):适用于一般系统,但系统遭到破坏后,会对公民、法人和其他组织的合法权益造成严重损害,或者对社会秩序和公共利益造成损害。本级在主管部门的指导下进行保护。
第三级(监督保护级):这是我们重点关注的级别。适用于涉及国家安全、社会秩序、公共利益的重要系统。系统遭到破坏后,会对社会秩序和公共利益造成严重损害,或者对国家安全造成损害。本级需要接受国家监督机构的强制监督和检查。
第四级(强制保护级):适用于涉及国家安全、社会秩序、公共利益的高度重要系统。系统遭到破坏后,会对社会秩序和公共利益造成特别严重损害,或者对国家安全造成严重损害。
第五级(专控保护级):适用于涉及国家安全的极端重要系统。系统遭到破坏后,会对国家安全造成特别严重损害。

三、大致的思路
以前端请求java 为例子。
1、登录接口,返回公钥。同时会也在token里面封装signKey字段。
2、前端生成sm4的32为hex的秘钥,然后用公钥加密(sm2的公钥加密算法),放在请求头encryptkey这个字段里边。
3、appSecretKey、token里面的signKey、时间戳、随机数、排序等一顿操作算出来sm3,也就是摘要。
4、把真实请求的json 加密,通过前端生成sm4的32为hex的秘钥(sm4的算法为:sm4/gcm/12 字节 IV/128bit TAG/AAD = GM_SM4_GCM/密文格式:ivHex:cipherHex/编码:UTF-8 / Hex 小写),
5、最终报文:
body:{
"encryptData":"bb6b2eb22d0d856d046e504e:040df7f7589a52f810bde30b4b68bb9898081b1b53f27efa810cea994de15cd77cfd0db9ae4f9e98fa30e25b4685271ab61f0e601fae8f55b7c68b2d71de68549b3b1370de276f926788bc7d07879ae94e8386d5d0cca2da2708471990d28afb50abeb0bcb0e2765dfa57b48c1e8d44d699363682fc9e7e4f051dec37e53889bcba25767eb02 "
}header:
sign:ce8f720217fc7f8d4b11948a59279953d02153d97cb24e844b2e24988b9af296
timestamp:1775210007906
nonce:d6U6HJesMJGVRujQ
encryptkey(前端生成sm4的32为hex的秘钥,然后用公钥加密):04e0bb36c2b7b02892004c231331cd5554cf221cbf455719a74da2188d70c25eb51597b6ca0ccd704753537bb34a4621a7e23ebbe9169c6375a348b5ee099dabc691def45b46e26fdb9aada27bbf476d07dba15e0817209079e5bed83478157b67d0fa12fbf6e1fb1cc0f06da3c9c5192fa900492ce7e6557e4a3d72d82be7c40f
四、具体代码示例
import Axios, { type AxiosInstance, type AxiosRequestConfig, type CustomParamsSerializer } from "axios";
import type { PureHttpError, RequestMethods, PureHttpResponse, PureHttpRequestConfig } from "./types.d";
import { stringify } from "qs";
import NProgress from "../progress";
import { getToken, formatToken, removeToken } from "@/utils/auth";
import { ElNotification, ElMessageBox } from "element-plus";
import router from "@/router";
import { jwtDecode } from "jwt-decode";
import { cloneDeep } from "lodash";
import settings from "@/settings";
import {
generateSm4Key,
sm2Encrypt,
sm4GcmEncrypt,
sm4GcmDecrypt,
getNowTimestamp,
generateNonce,
hmacSm3Sign
} from "@/utils/SM4-GCM";
import { useAppStore } from "@/store/modules/app";
const sm4KeyMap = {};
function getRequestIdFromConfig(headers: Record<string, unknown> | undefined): string {
if (!headers) return "";
const h = headers as Record<string, string>;
return h.requestId || h.requestid || h["Request-Id"] || "";
}
let isShowingTokenExpiredAlert = false;
const appSecretKey = "eca44a5a258c4c10a4b857f6111615f0";
const defaultConfig: AxiosRequestConfig = {
timeout: 10000,
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
paramsSerializer: {
serialize: stringify as unknown as CustomParamsSerializer
}
};
class PureHttp {
constructor() {
this.httpInterceptorsRequest();
this.httpInterceptorsResponse();
}
private static requests = [];
private static isRefreshing = false;
private static initConfig: PureHttpRequestConfig = {};
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
config.headers["Authorization"] = formatToken(token);
resolve(config);
});
});
}
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig): Promise<any> => {
NProgress.start();
const method = config.method?.toLowerCase();
const data = getToken();
if (["get", "delete"].includes(method)) {
const timestamp = new Date().getTime();
const hasQuery = config.url?.includes("?");
config.url = `${config.url}${hasQuery ? "&" : "?"}t=${timestamp}`;
}
if (config["ContentType"] === "multipart/form-data") {
config.headers["Content-Type"] = "multipart/form-data";
}
if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback(config);
return config;
}
if (PureHttp.initConfig.beforeRequestCallback) {
PureHttp.initConfig.beforeRequestCallback(config);
return config;
}
const whiteList = ["/refresh-token", "/login"];
if (whiteList.some(url => config.url?.endsWith(url))) {
return config;
}
if (!data?.token) return config;
config.headers["Authorization"] = formatToken(data.token);
if (settings.enabled) {
// 强制每个请求独立密钥
const sm4Key = generateSm4Key();
const requestId = "REQ_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
config.headers.requestId = requestId;
sm4KeyMap[requestId] = sm4Key;
const encryptkey = sm2Encrypt(sm4Key, useAppStore().getPublicKeyHex);
config.headers["encryptkey"] = encryptkey;
const payload = jwtDecode(data.token);
const signKey = payload.signKey;
const ts = getNowTimestamp();
const nonce = generateNonce();
let signParams = {};
if (method === "get" || method === "delete") {
const urlObj = new URL(config.url, window.location.origin);
urlObj.searchParams.forEach((val, key) => {
signParams[key] = val;
});
} else if (method === "post" || method === "put") {
if (config.data) {
signParams = cloneDeep(config.data);
const encrypted = sm4GcmEncrypt(JSON.stringify(config.data), sm4Key);
config.data = { encryptData: encrypted };
}
}
signParams.timestamp = ts;
signParams.nonce = nonce;
signParams.signKey = signKey;
const signStr = Object.keys(signParams)
.filter(k => signParams[k] != null)
.sort()
.map(k => {
const value =
Array.isArray(signParams[k]) || typeof signParams[k] === "object"
? JSON.stringify(signParams[k])
: signParams[k];
return `${k}=${value}`;
})
.join("&");
const signResult = hmacSm3Sign(signStr, appSecretKey);
config.headers["sign"] = signResult;
config.headers["nonce"] = nonce;
config.headers["timestamp"] = ts;
}
return config;
},
error => Promise.reject(error)
);
}
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResponse) => {
NProgress.done();
const data = response.data || {};
let realData = data;
if (data.encryptData && settings.enabled) {
try {
const requestId = getRequestIdFromConfig(response.config.headers as unknown as Record<string, unknown>);
const key = sm4KeyMap[requestId];
if (!key) {
console.error("[http] 解密失败:找不到 requestId 对应的 SM4 密钥", { requestId });
throw new Error("缺少会话密钥,请重试请求");
}
const decrypted = sm4GcmDecrypt(data.encryptData, key);
realData = JSON.parse(decrypted);
delete sm4KeyMap[requestId];
} catch (e) {
console.error("解密失败:", e);
throw e;
}
}
if (realData?.code === 401) {
if (isShowingTokenExpiredAlert) return Promise.reject(realData?.msg);
isShowingTokenExpiredAlert = true;
ElMessageBox.confirm("登录已过期,请重新登录", "提示", {
showCancelButton: false,
confirmButtonText: "确定",
type: "warning"
})
.then(() => {
removeToken();
router.replace("/login");
})
.finally(() => {
isShowingTokenExpiredAlert = false;
});
return Promise.reject(realData?.msg);
}
if (realData.code !== 200 && !(response.data instanceof Blob)) {
ElNotification({ message: realData?.msg, type: "error", duration: 1500 });
return Promise.reject(realData?.msg);
}
return response.config.url?.includes("/auditReport/export") ? response : settings.enabled ? realData : data;
},
(error: PureHttpError) => {
NProgress.done();
return Promise.reject(error);
}
);
}
public request<T>(
method: RequestMethods,
url: string,
param?: AxiosRequestConfig,
axiosConfig?: PureHttpRequestConfig
): Promise<T> {
const config = { method, url, ...param, ...axiosConfig } as PureHttpRequestConfig;
return new Promise((resolve, reject) => {
PureHttp.axiosInstance.request(config).then(resolve).catch(reject);
});
}
public post<T, P>(url: string, params?: P, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>("post", url, { data: params }, config);
}
public get<T, P>(url: string, params?: P, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>("get", url, { params: params }, config);
}
public delete<T, P>(url: string, params?: P, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>("delete", url, { params: params }, config);
}
public put<T, P>(url: string, params?: P, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>("put", url, { data: params }, config);
}
public getTime<T, P>(url: string, params?: P, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>("get", url, { params, timeout: 3000000 }, config);
}
}
export const http = new PureHttp();工具文件
import { sm4 } from "sm-crypto-v2";
import { sm2, sm3 } from "sm-crypto";
// ========== 1. 对称加密(前端使用) ==========
const GCM_IV_LENGTH = 12; // 必须12字节
const AAD_STR = "GM_SM4_GCM";
const GCM_TAG_LENGTH = 16; // 128bit = 16字节
/**
* 前端 SM4-GCM 加密(与 sm-crypto-v2 一致)
* @param {string} plainText 明文
* @param {string} keyBase64 16字节SM4密钥(base64)
* @returns {string} ivBase64:cipherWithTagBase64
*/
export function sm4GcmEncrypt(plainText, keyBase64) {
// 1. Base64 密钥 → Uint8Array(与 decrypt 侧一致)
const key = base64ToUint8(keyBase64);
// 2. 生成 12字节 IV
const iv = crypto.getRandomValues(new Uint8Array(GCM_IV_LENGTH));
// 3. 明文、AAD 转字节(必须用 associatedData,additionalData 会被库忽略)
const data = new TextEncoder().encode(plainText);
const aad = new TextEncoder().encode(AAD_STR);
// 4. GCM:output:"array" 且未设 outputTag 时只返回密文不含 tag,会导致与后端/tag 校验不一致
const enc = sm4.encrypt(data, key, {
mode: "gcm",
iv,
associatedData: aad,
output: "array",
outputTag: true
}) as { output: Uint8Array; tag: Uint8Array };
const cipherWithTag = concatUint8(enc.output, enc.tag);
const ivBase64 = uint8ToBase64(iv);
const cipherWithTagBase64 = uint8ToBase64(cipherWithTag);
return `${ivBase64}:${cipherWithTagBase64}`;
}
/**
* SM4-GCM 解密
* @param encryptedBase64
* @param sm4KeyBase64
* @returns
*/
export function sm4GcmDecrypt(encryptedBase64, sm4KeyBase64) {
try {
const [ivBase64, cipherWithTagBase64] = String(encryptedBase64 || "").split(":");
const iv = base64ToUint8(ivBase64);
const cipherWithTag = base64ToUint8(cipherWithTagBase64);
const key = base64ToUint8(sm4KeyBase64);
const aad = new TextEncoder().encode(AAD_STR);
const tag = cipherWithTag.slice(-GCM_TAG_LENGTH);
const cipherText = cipherWithTag.slice(0, -GCM_TAG_LENGTH);
const plainBytes = sm4.decrypt(cipherText, key, {
mode: "gcm",
iv,
tag,
associatedData: aad,
output: "array"
});
return new TextDecoder().decode(plainBytes as Uint8Array);
} catch (e) {
console.error("解密失败:", e);
throw e;
}
}
/**
* 生成 SM4 16字节密钥
* @returns Base64
*/
export function generateSm4Key() {
const randomBytes = crypto.getRandomValues(new Uint8Array(16));
return btoa(String.fromCharCode(...randomBytes));
}
// ========== 2. 公钥加密(前端使用) ==========
// @param text 明文字符串
// @param publicKey SM2公钥
// @return 加密后的十六进制字符串
export function sm2Encrypt(text, publicKeyBase64) {
// 1. 把【Base64公钥】转成 Hex公钥(sm2 加密必须用 Hex 公钥)
const publicKeyHex = base64ToHex(publicKeyBase64);
// 2. SM2 加密(C1C2C3 格式)
const hexCipher = sm2.doEncrypt(text, publicKeyHex, 1);
// 3. 拼接 04 前缀(后端通用格式)
const fullHex = "04" + hexCipher;
// 4. 转成最终的 Base64 密文返回
return hexToBase64(fullHex);
}
// ===================== 工具函数 =====================
// Base64 → Hex
function base64ToHex(base64) {
const uint8Array = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
return Array.from(uint8Array, b => b.toString(16).padStart(2, "0")).join("");
}
function base64ToUint8(base64: string): Uint8Array {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function concatUint8(a: Uint8Array, b: Uint8Array): Uint8Array {
const out = new Uint8Array(a.length + b.length);
out.set(a, 0);
out.set(b, a.length);
return out;
}
/** 大数组安全 Base64(避免 ... 展开参数长度限制) */
function uint8ToBase64(uint8: Uint8Array): string {
let binary = "";
for (let i = 0; i < uint8.length; i++) {
binary += String.fromCharCode(uint8[i]);
}
return btoa(binary);
}
export function getNowTimestamp() {
return Date.now();
}
export function generateNonce(length = 16) {
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
export function hmacSm3Sign(text: string, key: string): string {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(key);
const textBytes = encoder.encode(text);
const blockSize = 64;
let keyPadded = new Uint8Array(blockSize);
if (keyBytes.length > blockSize) {
const keyHash = hexToBytes(sm3(keyBytes));
keyPadded.set(keyHash);
} else {
keyPadded.set(keyBytes);
}
const ipad = new Uint8Array(blockSize).fill(0x36);
const opad = new Uint8Array(blockSize).fill(0x5c);
const ikey = xor(keyPadded, ipad);
const okey = xor(keyPadded, opad);
const inner = sm3(concat(ikey, textBytes));
const outer = sm3(concat(okey, hexToBytes(inner)));
return hexToBase64(outer.toLowerCase());
}
function hexToBase64(hex: string): string {
const bytes = new Uint8Array(hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
return btoa(String.fromCodePoint(...bytes));
}
function xor(a: Uint8Array, b: Uint8Array): Uint8Array {
const r = new Uint8Array(a.length);
for (let i = 0; i < a.length; i++) r[i] = a[i] ^ b[i];
return r;
}
function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
const r = new Uint8Array(a.length + b.length);
r.set(a);
r.set(b, a.length);
return r;
}
function hexToBytes(hex: string): Uint8Array {
const b = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) b[i / 2] = parseInt(hex.slice(i, i + 2), 16);
return b;
}