在数字经济时代,数据已经成为企业的核心资产,同时也是黑客攻击的核心目标。近年来,数据泄露事件频发,轻则导致用户隐私泄露、企业品牌受损,重则触发合规处罚,最高可处企业年营业额5%的罚款,相关责任人还需承担刑事责任。数据安全不是单一的技术点,而是覆盖数据全生命周期的体系化工程。
一、数据加密:数据安全的底层防线
加密是数据安全最核心的技术手段,其本质是通过不可逆或可逆的数学算法,将可读的明文数据转换为不可读的密文数据,仅授权持有合法密钥的主体可还原数据。根据加密逻辑的不同,可分为可逆加密(对称加密、非对称加密)与不可逆加密(哈希摘要)两大类,同时国内合规场景需优先支持国密算法。
1.1 对称加密:大批量数据加密的首选
对称加密的核心逻辑是加密与解密使用同一把密钥,其优势是加解密速度极快、资源消耗低,适合大文件、大批量业务数据的加密场景;核心短板是密钥分发与管理难度高,一旦密钥泄露,所有加密数据将完全暴露。
核心算法与安全规范
- 国际标准算法:AES(高级加密标准),目前安全合规的密钥长度为256位,加密模式必须使用GCM(伽罗瓦/计数器模式)。GCM是带认证的加密模式,可同时实现数据加密与完整性校验,彻底避免ECB模式(电子密码本模式)的明文重放攻击风险。
- 国密标准算法:SM4,是我国商用密码管理局发布的对称加密标准,密钥长度128位,分组长度128位,安全强度与AES-128相当,是国内等保2.0、金融、政务等合规场景的强制要求。
核心依赖
<!-- 国密算法支持 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<!-- Spring核心工具类 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.1.5</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.12</version>
</dependency>
AES-256-GCM加密工具类
package com.jam.demo.security.encrypt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
/**
* AES对称加密工具类
* 算法:AES-256-GCM
* @author ken
*/
@Slf4j
public class AesEncryptUtil {
private static final String ALGORITHM_NAME = "AES";
private static final String ALGORITHM_MODE = "AES/GCM/NoPadding";
private static final int KEY_LENGTH = 256;
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
/**
* 生成AES-256位密钥
* @return Base64编码的密钥字符串
* @throws Exception 密钥生成异常
*/
public static String generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM_NAME);
keyGenerator.init(KEY_LENGTH, new SecureRandom());
SecretKey secretKey = keyGenerator.generateKey();
return BASE64_ENCODER.encodeToString(secretKey.getEncoded());
}
/**
* AES-GCM加密
* @param plainText 明文
* @param base64Key Base64编码的密钥
* @return Base64编码的密文(包含IV)
* @throws Exception 加密异常
*/
public static String encrypt(String plainText, String base64Key) throws Exception {
if (!StringUtils.hasText(plainText)) {
return plainText;
}
if (!StringUtils.hasText(base64Key)) {
throw new IllegalArgumentException("加密密钥不能为空");
}
byte[] keyBytes = BASE64_DECODER.decode(base64Key);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM_NAME);
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, parameterSpec);
byte[] cipherBytes = cipher.doFinal(plainText.getBytes());
byte[] combined = new byte[iv.length + cipherBytes.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(cipherBytes, 0, combined, iv.length, cipherBytes.length);
return BASE64_ENCODER.encodeToString(combined);
}
/**
* AES-GCM解密
* @param cipherText Base64编码的密文(包含IV)
* @param base64Key Base64编码的密钥
* @return 解密后的明文
* @throws Exception 解密异常
*/
public static String decrypt(String cipherText, String base64Key) throws Exception {
if (!StringUtils.hasText(cipherText)) {
return cipherText;
}
if (!StringUtils.hasText(base64Key)) {
throw new IllegalArgumentException("解密密钥不能为空");
}
byte[] combined = BASE64_DECODER.decode(cipherText);
if (combined.length < GCM_IV_LENGTH) {
throw new IllegalArgumentException("非法的密文格式");
}
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, iv.length);
byte[] cipherBytes = new byte[combined.length - iv.length];
System.arraycopy(combined, iv.length, cipherBytes, 0, cipherBytes.length);
byte[] keyBytes = BASE64_DECODER.decode(base64Key);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM_NAME);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, parameterSpec);
byte[] plainBytes = cipher.doFinal(cipherBytes);
return new String(plainBytes);
}
}
SM4国密对称加密工具类
package com.jam.demo.security.encrypt;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
import java.security.SecureRandom;
import java.util.Base64;
/**
* SM4国密对称加密工具类
* 算法:SM4-GCM
* @author ken
*/
@Slf4j
public class Sm4EncryptUtil {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final String ALGORITHM_NAME = "SM4";
private static final String ALGORITHM_MODE = "SM4/GCM/NoPadding";
private static final int KEY_LENGTH = 128;
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
/**
* 生成SM4密钥
* @return Base64编码的密钥字符串
* @throws Exception 密钥生成异常
*/
public static String generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
keyGenerator.init(KEY_LENGTH, new SecureRandom());
SecretKey secretKey = keyGenerator.generateKey();
return BASE64_ENCODER.encodeToString(secretKey.getEncoded());
}
/**
* SM4-GCM加密
* @param plainText 明文
* @param base64Key Base64编码的密钥
* @return Base64编码的密文(包含IV)
* @throws Exception 加密异常
*/
public static String encrypt(String plainText, String base64Key) throws Exception {
if (!StringUtils.hasText(plainText)) {
return plainText;
}
if (!StringUtils.hasText(base64Key)) {
throw new IllegalArgumentException("加密密钥不能为空");
}
byte[] keyBytes = BASE64_DECODER.decode(base64Key);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM_NAME);
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE, BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, parameterSpec);
byte[] cipherBytes = cipher.doFinal(plainText.getBytes());
byte[] combined = new byte[iv.length + cipherBytes.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(cipherBytes, 0, combined, iv.length, cipherBytes.length);
return BASE64_ENCODER.encodeToString(combined);
}
/**
* SM4-GCM解密
* @param cipherText Base64编码的密文(包含IV)
* @param base64Key Base64编码的密钥
* @return 解密后的明文
* @throws Exception 解密异常
*/
public static String decrypt(String cipherText, String base64Key) throws Exception {
if (!StringUtils.hasText(cipherText)) {
return cipherText;
}
if (!StringUtils.hasText(base64Key)) {
throw new IllegalArgumentException("解密密钥不能为空");
}
byte[] combined = BASE64_DECODER.decode(cipherText);
if (combined.length < GCM_IV_LENGTH) {
throw new IllegalArgumentException("非法的密文格式");
}
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, iv.length);
byte[] cipherBytes = new byte[combined.length - iv.length];
System.arraycopy(combined, iv.length, cipherBytes, 0, cipherBytes.length);
byte[] keyBytes = BASE64_DECODER.decode(base64Key);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM_NAME);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE, BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, parameterSpec);
byte[] plainBytes = cipher.doFinal(cipherBytes);
return new String(plainBytes);
}
}
1.2 非对称加密:密钥分发与身份认证的核心
非对称加密的核心逻辑是生成一对数学关联的密钥:公钥与私钥。公钥可对外公开分发,私钥必须由持有者严格保密。核心使用场景分为两类:
- 加密场景:公钥加密,私钥解密。解决对称加密的密钥分发难题,可用于加密对称密钥、小批量敏感数据。
- 签名场景:私钥签名,公钥验签。实现身份认证与数据防篡改,可用于接口签名、数字证书、交易验签等场景。
核心算法与安全规范
- 国际标准算法:RSA,安全合规的密钥长度为2048位及以上,加密填充模式必须使用OAEPWithSHA-256AndMGF1Padding,签名填充模式必须使用PSS,彻底避免PKCS#1 v1.5填充的安全漏洞。
- 国密标准算法:SM2,是我国商用密码管理局发布的非对称加密标准,基于椭圆曲线密码算法,安全强度与RSA-3072相当,计算效率更高,是国内合规场景的强制要求。
SM2国密非对称加密工具类
package com.jam.demo.security.encrypt;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.StringUtils;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;
/**
* SM2国密非对称加密工具类
* 支持公钥加密、私钥解密、私钥签名、公钥验签
* @author ken
*/
@Slf4j
public class Sm2EncryptUtil {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final X9ECParameters SM2_CURVE = GMNamedCurves.getByName("sm2p256v1");
private static final ECDomainParameters SM2_DOMAIN_PARAMS = new ECDomainParameters(SM2_CURVE.getCurve(), SM2_CURVE.getG(), SM2_CURVE.getN());
private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
/**
* 生成SM2密钥对
* @return 密钥对,索引0为私钥,索引1为公钥(Base64编码)
* @throws Exception 密钥生成异常
*/
public static String[] generateKeyPair() throws Exception {
ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator();
keyPairGenerator.init(new ECKeyGenerationParameters(SM2_DOMAIN_PARAMS, new SecureRandom()));
AsymmetricCipherKeyPair keyPair = keyPairGenerator.generateKeyPair();
ECPrivateKeyParameters privateKeyParams = (ECPrivateKeyParameters) keyPair.getPrivate();
ECPublicKeyParameters publicKeyParams = (ECPublicKeyParameters) keyPair.getPublic();
String privateKey = BASE64_ENCODER.encodeToString(privateKeyParams.getD().toByteArray());
String publicKey = BASE64_ENCODER.encodeToString(publicKeyParams.getQ().getEncoded(false));
return new String[]{privateKey, publicKey};
}
/**
* SM2公钥加密
* @param plainText 明文
* @param base64PublicKey Base64编码的公钥
* @return Base64编码的密文
* @throws Exception 加密异常
*/
public static String encrypt(String plainText, String base64PublicKey) throws Exception {
if (!StringUtils.hasText(plainText)) {
return plainText;
}
if (!StringUtils.hasText(base64PublicKey)) {
throw new IllegalArgumentException("加密公钥不能为空");
}
byte[] publicKeyBytes = BASE64_DECODER.decode(base64PublicKey);
ECPublicKeyParameters publicKeyParams = new ECPublicKeyParameters(SM2_CURVE.getCurve().decodePoint(publicKeyBytes), SM2_DOMAIN_PARAMS);
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(true, new ParametersWithRandom(publicKeyParams, new SecureRandom()));
byte[] cipherBytes = sm2Engine.processBlock(plainText.getBytes(), 0, plainText.getBytes().length);
return BASE64_ENCODER.encodeToString(cipherBytes);
}
/**
* SM2私钥解密
* @param cipherText Base64编码的密文
* @param base64PrivateKey Base64编码的私钥
* @return 解密后的明文
* @throws Exception 解密异常
*/
public static String decrypt(String cipherText, String base64PrivateKey) throws Exception {
if (!StringUtils.hasText(cipherText)) {
return cipherText;
}
if (!StringUtils.hasText(base64PrivateKey)) {
throw new IllegalArgumentException("解密私钥不能为空");
}
byte[] privateKeyBytes = BASE64_DECODER.decode(base64PrivateKey);
ECPrivateKeyParameters privateKeyParams = new ECPrivateKeyParameters(new java.math.BigInteger(1, privateKeyBytes), SM2_DOMAIN_PARAMS);
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(false, privateKeyParams);
byte[] plainBytes = sm2Engine.processBlock(BASE64_DECODER.decode(cipherText), 0, BASE64_DECODER.decode(cipherText).length);
return new String(plainBytes);
}
}
1.3 不可逆哈希算法:数据完整性与密码存储的核心
哈希算法的核心逻辑是将任意长度的输入数据,通过单向哈希函数转换为固定长度的输出摘要,其核心特性是:
- 单向不可逆:无法通过摘要反推原始输入数据
- 雪崩效应:原始输入的微小变化,会导致输出摘要发生巨大变化
- 唯一性:相同的输入必然得到相同的输出摘要
核心算法与安全规范
- 数据完整性校验:国际标准用SHA-256,国密标准用SM3,禁止使用MD5、SHA-1等已被破解的算法。
- 密码存储:必须使用带盐值的慢哈希算法,包括BCrypt、PBKDF2、Argon2,禁止直接使用SHA-256、SM3等快速哈希算法存储密码,防止彩虹表攻击。慢哈希算法的核心是通过多次迭代计算,大幅增加暴力破解的时间成本。
哈希摘要与密码加密工具类
package com.jam.demo.security.encrypt;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.util.StringUtils;
import java.security.MessageDigest;
import java.security.Security;
import java.util.Base64;
/**
* 哈希摘要工具类
* 支持SM3国密摘要、BCrypt密码加密
* @author ken
*/
@Slf4j
public class HashDigestUtil {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final String SM3_ALGORITHM = "SM3";
private static final BCryptPasswordEncoder BCRYPT_ENCODER = new BCryptPasswordEncoder(12);
private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
/**
* SM3国密摘要计算
* @param input 输入字符串
* @return Base64编码的SM3摘要
* @throws Exception 摘要计算异常
*/
public static String sm3Digest(String input) throws Exception {
if (!StringUtils.hasText(input)) {
return input;
}
MessageDigest messageDigest = MessageDigest.getInstance(SM3_ALGORITHM, BouncyCastleProvider.PROVIDER_NAME);
byte[] digestBytes = messageDigest.digest(input.getBytes());
return BASE64_ENCODER.encodeToString(digestBytes);
}
/**
* BCrypt密码加密
* @param rawPassword 原始密码
* @return 加密后的密码哈希
*/
public static String bcryptEncrypt(String rawPassword) {
if (!StringUtils.hasText(rawPassword)) {
return rawPassword;
}
return BCRYPT_ENCODER.encode(rawPassword);
}
/**
* BCrypt密码校验
* @param rawPassword 原始密码
* @param encodedPassword 加密后的密码哈希
* @return 校验结果
*/
public static boolean bcryptMatches(String rawPassword, String encodedPassword) {
if (!StringUtils.hasText(rawPassword) || !StringUtils.hasText(encodedPassword)) {
return false;
}
return BCRYPT_ENCODER.matches(rawPassword, encodedPassword);
}
}
二、数据脱敏:敏感数据的隐私保护屏障
数据脱敏是指对敏感数据进行变形、替换、屏蔽处理,在不影响业务逻辑正常执行的前提下,降低数据的敏感级别,防止非授权访问、数据导出、测试环境使用等场景下的隐私数据泄露。 根据应用场景的不同,数据脱敏分为两大类:
- 静态脱敏:对数据进行离线的、永久性的脱敏处理,主要用于生产数据同步到测试环境、数据仓库、数据分析平台等场景,确保非生产环境不存在真实的敏感数据。
- 动态脱敏:在数据访问时实时进行脱敏处理,主要用于生产环境的接口返回、数据查询、报表展示等场景,根据访问者的权限级别,返回不同脱敏程度的数据,实现最小权限访问。
2.1 通用脱敏规则与场景
针对不同类型的敏感数据,行业内有通用的合规脱敏规则,既保证数据的不可识别性,又保留部分业务所需的格式特征:
| 数据类型 | 脱敏规则 | 示例 |
| 手机号 | 保留前3位、后4位,中间4位用*替换 | 138****1234 |
| 身份证号 | 保留前6位、后4位,中间8位用*替换 | 310101********1234 |
| 银行卡号 | 保留前6位、后4位,中间位数用*替换 | 622202********1234 |
| 姓名 | 保留姓氏,其余用*替换 | 张* |
| 邮箱地址 | 保留@前1位、@后完整域名,其余用*替换 | z***@163.com |
| 收货地址 | 保留省市级信息,详细地址用*替换 | 上海市浦东新区**** |
2.2 动态脱敏实战实现
动态脱敏的核心是在数据返回给访问者之前,根据权限实时完成脱敏处理,主流的实现方式有两种:MyBatisPlus结果集拦截脱敏(数据库查询层)、Jackson序列化脱敏(接口返回层),两种方式可结合使用,实现全链路的脱敏防护。
核心依赖
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.36</version>
</dependency>
<!-- SpringDoc OpenAPI 3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.4</version>
</dependency>
<!-- Spring Security Crypto -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>6.2.3</version>
</dependency>
<!-- Guava集合工具 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.1.0-jre</version>
</dependency>
<!-- Fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.49</version>
</dependency>
脱敏类型枚举
package com.jam.demo.security.desensitize;
/**
* 脱敏类型枚举
* @author ken
*/
public enum DesensitizeType {
/**
* 手机号
*/
PHONE,
/**
* 身份证号
*/
ID_CARD,
/**
* 银行卡号
*/
BANK_CARD,
/**
* 姓名
*/
NAME,
/**
* 邮箱
*/
EMAIL,
/**
* 地址
*/
ADDRESS,
/**
* 自定义
*/
CUSTOM
}
脱敏注解
package com.jam.demo.security.desensitize;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
/**
* 数据脱敏注解
* 支持Jackson序列化脱敏与MyBatisPlus结果集脱敏
* @author ken
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizeSerializer.class)
public @interface Desensitize {
/**
* 脱敏类型
*/
DesensitizeType type();
/**
* 前置保留位数(自定义类型生效)
*/
int prefixKeep() default 0;
/**
* 后置保留位数(自定义类型生效)
*/
int suffixKeep() default 0;
/**
* 脱敏替换符
*/
char replacer() default '*';
}
脱敏工具类
package com.jam.demo.security.desensitize;
import org.springframework.util.StringUtils;
/**
* 数据脱敏工具类
* @author ken
*/
public class DesensitizeUtil {
private static final char DEFAULT_REPLACER = '*';
/**
* 通用脱敏方法
* @param content 原始内容
* @param prefixKeep 前置保留位数
* @param suffixKeep 后置保留位数
* @param replacer 替换符
* @return 脱敏后的内容
*/
public static String desensitize(String content, int prefixKeep, int suffixKeep, char replacer) {
if (!StringUtils.hasText(content)) {
return content;
}
int contentLength = content.length();
if (contentLength <= prefixKeep + suffixKeep) {
return content;
}
StringBuilder sb = new StringBuilder(contentLength);
sb.append(content, 0, prefixKeep);
int replaceLength = contentLength - prefixKeep - suffixKeep;
sb.append(String.valueOf(replacer).repeat(replaceLength));
sb.append(content.substring(contentLength - suffixKeep));
return sb.toString();
}
/**
* 手机号脱敏
* @param phone 手机号
* @return 脱敏后的手机号
*/
public static String desensitizePhone(String phone) {
return desensitize(phone, 3, 4, DEFAULT_REPLACER);
}
/**
* 身份证号脱敏
* @param idCard 身份证号
* @return 脱敏后的身份证号
*/
public static String desensitizeIdCard(String idCard) {
return desensitize(idCard, 6, 4, DEFAULT_REPLACER);
}
/**
* 银行卡号脱敏
* @param bankCard 银行卡号
* @return 脱敏后的银行卡号
*/
public static String desensitizeBankCard(String bankCard) {
return desensitize(bankCard, 6, 4, DEFAULT_REPLACER);
}
/**
* 姓名脱敏
* @param name 姓名
* @return 脱敏后的姓名
*/
public static String desensitizeName(String name) {
if (!StringUtils.hasText(name)) {
return name;
}
return desensitize(name, 1, 0, DEFAULT_REPLACER);
}
/**
* 邮箱脱敏
* @param email 邮箱
* @return 脱敏后的邮箱
*/
public static String desensitizeEmail(String email) {
if (!StringUtils.hasText(email)) {
return email;
}
int atIndex = email.indexOf('@');
if (atIndex <= 1) {
return email;
}
String prefix = desensitize(email.substring(0, atIndex), 1, 0, DEFAULT_REPLACER);
return prefix + email.substring(atIndex);
}
/**
* 地址脱敏
* @param address 地址
* @return 脱敏后的地址
*/
public static String desensitizeAddress(String address) {
if (!StringUtils.hasText(address)) {
return address;
}
return desensitize(address, 6, 0, DEFAULT_REPLACER);
}
}
数据安全上下文持有者
package com.jam.demo.security.desensitize;
/**
* 数据安全上下文持有者
* 用于传递当前用户的权限信息
* @author ken
*/
public class DataSecurityContextHolder {
private static final ThreadLocal<DataSecurityContext> CONTEXT_HOLDER = new ThreadLocal<>();
private DataSecurityContextHolder() {
}
public static void setContext(DataSecurityContext context) {
CONTEXT_HOLDER.set(context);
}
public static DataSecurityContext getContext() {
return CONTEXT_HOLDER.get();
}
public static void clear() {
CONTEXT_HOLDER.remove();
}
public static boolean isAdmin() {
DataSecurityContext context = getContext();
return context != null && context.isAdmin();
}
public static class DataSecurityContext {
private final Long userId;
private final boolean isAdmin;
public DataSecurityContext(Long userId, boolean isAdmin) {
this.userId = userId;
this.isAdmin = isAdmin;
}
public Long getUserId() {
return userId;
}
public boolean isAdmin() {
return isAdmin;
}
}
}
Jackson脱敏序列化器
package com.jam.demo.security.desensitize;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
/**
* 脱敏序列化器
* 用于Jackson序列化时自动脱敏
* @author ken
*/
@Slf4j
public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {
private DesensitizeType desensitizeType;
private int prefixKeep;
private int suffixKeep;
private char replacer;
public DesensitizeSerializer() {
}
public DesensitizeSerializer(DesensitizeType desensitizeType, int prefixKeep, int suffixKeep, char replacer) {
this.desensitizeType = desensitizeType;
this.prefixKeep = prefixKeep;
this.suffixKeep = suffixKeep;
this.replacer = replacer;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (!StringUtils.hasText(value)) {
gen.writeString(value);
return;
}
if (DataSecurityContextHolder.isAdmin()) {
gen.writeString(value);
return;
}
String desensitizedValue = switch (desensitizeType) {
case PHONE -> DesensitizeUtil.desensitizePhone(value);
case ID_CARD -> DesensitizeUtil.desensitizeIdCard(value);
case BANK_CARD -> DesensitizeUtil.desensitizeBankCard(value);
case NAME -> DesensitizeUtil.desensitizeName(value);
case EMAIL -> DesensitizeUtil.desensitizeEmail(value);
case ADDRESS -> DesensitizeUtil.desensitizeAddress(value);
case CUSTOM -> DesensitizeUtil.desensitize(value, prefixKeep, suffixKeep, replacer);
};
gen.writeString(desensitizedValue);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
if (ObjectUtils.isEmpty(property)) {
return prov.findNullValueSerializer(null);
}
Class<?> type = property.getType().getRawClass();
if (String.class != type) {
return prov.findValueSerializer(type, property);
}
Desensitize desensitize = property.getAnnotation(Desensitize.class);
if (!ObjectUtils.isEmpty(desensitize)) {
return new DesensitizeSerializer(desensitize.type(), desensitize.prefixKeep(), desensitize.suffixKeep(), desensitize.replacer());
}
return prov.findValueSerializer(type, property);
}
@Override
public Class<String> handledType() {
return String.class;
}
}
MyBatisPlus结果集脱敏拦截器
package com.jam.demo.security.desensitize;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
/**
* MyBatisPlus结果集脱敏拦截器
* 用于查询结果返回时自动脱敏
* @author ken
*/
@Slf4j
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DesensitizeResultSetInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (DataSecurityContextHolder.isAdmin()) {
return result;
}
if (result instanceof List<?> resultList) {
if (ObjectUtils.isEmpty(resultList)) {
return result;
}
for (Object row : resultList) {
desensitizeObject(row);
}
} else {
desensitizeObject(result);
}
return result;
}
private void desensitizeObject(Object obj) {
if (ObjectUtils.isEmpty(obj)) {
return;
}
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Desensitize desensitize = field.getAnnotation(Desensitize.class);
if (ObjectUtils.isEmpty(desensitize) || field.getType() != String.class) {
continue;
}
ReflectionUtils.makeAccessible(field);
try {
String value = (String) field.get(obj);
if (!ObjectUtils.isEmpty(value)) {
String desensitizedValue = switch (desensitize.type()) {
case PHONE -> DesensitizeUtil.desensitizePhone(value);
case ID_CARD -> DesensitizeUtil.desensitizeIdCard(value);
case BANK_CARD -> DesensitizeUtil.desensitizeBankCard(value);
case NAME -> DesensitizeUtil.desensitizeName(value);
case EMAIL -> DesensitizeUtil.desensitizeEmail(value);
case ADDRESS -> DesensitizeUtil.desensitizeAddress(value);
case CUSTOM -> DesensitizeUtil.desensitize(value, desensitize.prefixKeep(), desensitize.suffixKeep(), desensitize.replacer());
};
field.set(obj, desensitizedValue);
}
} catch (Exception e) {
log.error("字段脱敏失败,字段名:{}", field.getName(), e);
}
}
}
@Override
public Object plugin(Object target) {
if (target instanceof ResultSetHandler) {
return Plugin.wrap(target, this);
}
return target;
}
}
用户信息实体与数据库表结构
CREATE TABLE `t_user_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(64) NOT NULL COMMENT '用户姓名',
`phone` varchar(11) NOT NULL COMMENT '用户手机号',
`id_card` varchar(18) NOT NULL COMMENT '身份证号',
`bank_card` varchar(32) NOT NULL COMMENT '银行卡号',
`email` varchar(128) NOT NULL COMMENT '邮箱地址',
`address` varchar(256) NOT NULL COMMENT '收货地址',
`password_hash` varchar(128) NOT NULL COMMENT '密码哈希',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`),
UNIQUE KEY `uk_id_card` (`id_card`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表';
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.jam.demo.security.desensitize.Desensitize;
import com.jam.demo.security.desensitize.DesensitizeType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 用户信息实体
* @author ken
*/
@Data
@TableName("t_user_info")
@Schema(description = "用户信息实体")
public class UserInfo {
@TableId(type = IdType.AUTO)
@Schema(description = "用户ID", example = "1")
private Long id;
@Desensitize(type = DesensitizeType.NAME)
@Schema(description = "用户姓名", example = "张三")
private String userName;
@Desensitize(type = DesensitizeType.PHONE)
@Schema(description = "用户手机号", example = "13812345678")
private String phone;
@Desensitize(type = DesensitizeType.ID_CARD)
@Schema(description = "身份证号", example = "310101199001011234")
private String idCard;
@Desensitize(type = DesensitizeType.BANK_CARD)
@Schema(description = "银行卡号", example = "62220212345678901234")
private String bankCard;
@Desensitize(type = DesensitizeType.EMAIL)
@Schema(description = "邮箱地址", example = "zhangsan@163.com")
private String email;
@Desensitize(type = DesensitizeType.ADDRESS)
@Schema(description = "收货地址", example = "上海市浦东新区张江高科技园区博云路2号")
private String address;
@Schema(description = "密码哈希", example = "$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
private String passwordHash;
}
三、数据分级存储:安全、性能与成本的平衡之道
数据分级存储的核心逻辑是根据数据的敏感级别、业务价值、访问频率、合规要求,对数据进行科学的分类分级,为不同级别的数据匹配差异化的存储架构、加密策略、访问控制、备份方案,在满足数据安全与合规要求的前提下,平衡系统性能与存储成本。
3.1 数据分级的合规标准与划分规则
根据《中华人民共和国数据安全法》的要求,数据分级需遵循“谁收集、谁负责,谁使用、谁负责”的原则,结合行业规范,通用的四级数据分级标准如下:
- 绝密级数据:核心商业秘密、国家级敏感数据、企业核心知识产权,如核心算法源码、用户支付密码哈希、企业核心财务数据、国家秘密相关数据。此类数据一旦泄露,将对企业、国家造成毁灭性的损害。
- 机密级数据:重要商业秘密、个人敏感信息(PII),如用户身份证号、银行卡号、生物识别信息、企业客户核心资料、未公开的商业计划。此类数据一旦泄露,将触发合规处罚,严重影响企业品牌与用户权益。
- 敏感级数据:一般商业信息、个人一般信息,如用户收货地址、订单信息、浏览记录、企业内部运营数据。此类数据一旦泄露,会对企业与用户造成一定的影响。
- 公开级数据:可对外公开的信息,如商品信息、企业公开资质、新闻公告、公开的行业数据。此类数据无敏感属性,可对外公开分发。
3.2 分级存储的架构设计
不同级别的数据,需匹配差异化的存储架构与安全策略,整体架构如下:
各级数据的存储与安全策略
- 绝密级数据
- 存储要求:采用专属物理隔离的存储集群,禁止与其他业务共用存储资源;优先使用硬件加密机(HSM)进行加密,密钥由专人管理,采用多人分权控制;禁止远程直接访问,仅通过专属加密服务接口访问。
- 访问控制:采用双因素认证,访问需双人授权;所有操作全程加密,审计日志永久留存;禁止数据导出、拷贝,仅支持业务必需的计算操作。
- 备份方案:采用离线加密备份,备份介质专人保管,备份数据仅可在专属隔离环境中恢复。
- 机密级数据
- 存储要求:采用独立的敏感数据存储集群,与普通业务数据物理隔离;对敏感字段进行字段级加密存储,密钥由KMS系统统一管理;禁止明文存储敏感数据。
- 访问控制:采用细粒度的RBAC权限控制,遵循最小权限原则;所有访问操作需记录审计日志,包括访问人、访问时间、访问内容、访问IP;动态脱敏仅对授权用户返回完整数据。
- 备份方案:采用加密备份,备份数据需进行静态脱敏,仅授权人员可恢复。
- 敏感级数据
- 存储要求:采用业务常规存储集群,支持逻辑隔离;传输过程全程采用TLS加密,禁止明文传输;支持数据访问频率统计,为冷热数据分离提供依据。
- 访问控制:采用常规的角色权限控制,禁止越权访问;操作日志留存不少于6个月。
- 备份方案:采用常规备份策略,根据业务需求设置备份周期。
- 公开级数据
- 存储要求:采用分布式存储集群,配合CDN加速与分布式缓存,提升访问性能;支持高并发访问,无需加密存储。
- 访问控制:无访问权限限制,仅需做访问频率控制,防止CC攻击。
- 备份方案:采用分布式多副本备份,无需额外加密。
3.3 分级存储的实战实现
分级存储的核心落地能力包括:字段级加解密存储、多数据源分级存储、冷热数据自动分层。其中最常用的是字段级加解密存储,通过MyBatisPlus的TypeHandler实现,在数据插入数据库时自动加密,查询时自动解密,对业务代码无侵入。
加密字段注解
package com.jam.demo.security.storage;
import java.lang.annotation.*;
/**
* 加密字段注解
* 用于标记需要加密存储的字段
* @author ken
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptField {
}
字段加密类型处理器
package com.jam.demo.security.storage;
import com.jam.demo.security.encrypt.Sm4EncryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 字段加密类型处理器
* 用于MyBatisPlus字段级自动加解密
* @author ken
*/
@Slf4j
@Component
@MappedTypes(String.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class EncryptTypeHandler extends BaseTypeHandler<String> {
@Value("${data.security.encrypt.key:}")
private String encryptKey;
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
if (!StringUtils.hasText(parameter)) {
ps.setString(i, parameter);
return;
}
try {
String encryptedValue = Sm4EncryptUtil.encrypt(parameter, encryptKey);
ps.setString(i, encryptedValue);
} catch (Exception e) {
log.error("字段加密失败", e);
throw new SQLException("字段加密失败", e);
}
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return decryptValue(value);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return decryptValue(value);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return decryptValue(value);
}
private String decryptValue(String value) {
if (!StringUtils.hasText(value)) {
return value;
}
try {
return Sm4EncryptUtil.decrypt(value, encryptKey);
} catch (Exception e) {
log.error("字段解密失败", e);
return value;
}
}
}
加密存储的用户实体改造
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.jam.demo.security.desensitize.Desensitize;
import com.jam.demo.security.desensitize.DesensitizeType;
import com.jam.demo.security.storage.EncryptField;
import com.jam.demo.security.storage.EncryptTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 用户信息实体
* @author ken
*/
@Data
@TableName("t_user_info")
@Schema(description = "用户信息实体")
public class UserInfo {
@TableId(type = IdType.AUTO)
@Schema(description = "用户ID", example = "1")
private Long id;
@Desensitize(type = DesensitizeType.NAME)
@Schema(description = "用户姓名", example = "张三")
private String userName;
@EncryptField
@TableField(typeHandler = EncryptTypeHandler.class)
@Desensitize(type = DesensitizeType.PHONE)
@Schema(description = "用户手机号", example = "13812345678")
private String phone;
@EncryptField
@TableField(typeHandler = EncryptTypeHandler.class)
@Desensitize(type = DesensitizeType.ID_CARD)
@Schema(description = "身份证号", example = "310101199001011234")
private String idCard;
@EncryptField
@TableField(typeHandler = EncryptTypeHandler.class)
@Desensitize(type = DesensitizeType.BANK_CARD)
@Schema(description = "银行卡号", example = "62220212345678901234")
private String bankCard;
@EncryptField
@TableField(typeHandler = EncryptTypeHandler.class)
@Desensitize(type = DesensitizeType.EMAIL)
@Schema(description = "邮箱地址", example = "zhangsan@163.com")
private String email;
@Desensitize(type = DesensitizeType.ADDRESS)
@Schema(description = "收货地址", example = "上海市浦东新区张江高科技园区博云路2号")
private String address;
@Schema(description = "密码哈希", example = "$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
private String passwordHash;
}
配置文件加密密钥配置
data:
security:
encrypt:
key: 你的SM4密钥Base64字符串
四、数据合规:数据安全的底线与红线
数据合规是数据安全的前提,所有数据安全措施的最终目标,都是满足法律法规的要求,规避合规风险。国内数据合规的核心法律法规包括《中华人民共和国数据安全法》《中华人民共和国个人信息保护法》《中华人民共和国网络安全法》,以及《网络安全等级保护条例》等配套规范。
4.1 合规的核心原则与要求
- 最小必要原则 收集、使用个人信息,必须限于实现业务功能的最小范围,不得过度收集、过度使用。例如,一个资讯类APP,不得强制收集用户的手机号、通讯录、位置信息等非必需的个人信息。
- 知情同意原则 收集个人信息前,必须以清晰、易懂的方式告知用户收集的目的、方式、范围、存储期限、使用规则等内容,获得用户的明确同意。用户有权撤回同意,企业需提供便捷的撤回渠道。
- 全生命周期安全原则 数据安全需覆盖数据的全生命周期,包括收集、传输、存储、使用、加工、传输、提供、公开、销毁等所有环节,每个环节都需有对应的安全防护措施。
- 权责一致原则 谁收集、谁负责,谁使用、谁负责。数据处理者对数据处理活动的合规性负责,发生数据安全事件时,需承担对应的法律责任。
- 可审计可追溯原则 所有对敏感数据的处理操作,都需有完整的审计日志,记录操作人、操作时间、操作类型、操作内容、操作IP等信息,日志留存时间不少于6个月,确保所有操作可追溯。
4.2 合规落地的核心实战能力
4.2.1 数据操作审计日志
审计日志是合规的核心要求,也是数据安全事件追溯的核心依据。通过AOP实现对敏感数据操作的全链路审计,对业务代码无侵入。
敏感操作审计注解
package com.jam.demo.security.audit;
import java.lang.annotation.*;
/**
* 敏感操作审计注解
* 用于标记需要审计的敏感数据操作方法
* @author ken
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SensitiveAudit {
/**
* 操作描述
*/
String operation();
/**
* 敏感数据类型
*/
String dataType();
}
审计日志实体与数据库表结构
CREATE TABLE `t_sensitive_audit_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',
`user_id` bigint NOT NULL COMMENT '操作人ID',
`user_name` varchar(64) NOT NULL COMMENT '操作人姓名',
`operation` varchar(128) NOT NULL COMMENT '操作描述',
`data_type` varchar(64) NOT NULL COMMENT '敏感数据类型',
`operation_ip` varchar(64) NOT NULL COMMENT '操作IP',
`request_method` varchar(256) NOT NULL COMMENT '请求方法',
`request_params` text COMMENT '请求参数',
`operation_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_operation_time` (`operation_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='敏感操作审计日志表';
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审计日志实体
* @author ken
*/
@Data
@TableName("t_sensitive_audit_log")
@Schema(description = "敏感操作审计日志实体")
public class SensitiveAuditLog {
@TableId(type = IdType.AUTO)
@Schema(description = "日志ID", example = "1")
private Long id;
@Schema(description = "操作人ID", example = "1")
private Long userId;
@Schema(description = "操作人姓名", example = "张三")
private String userName;
@Schema(description = "操作描述", example = "查询用户信息")
private String operation;
@Schema(description = "敏感数据类型", example = "用户个人信息")
private String dataType;
@Schema(description = "操作IP", example = "192.168.1.1")
private String operationIp;
@Schema(description = "请求方法", example = "com.jam.demo.controller.UserController.getUserInfo")
private String requestMethod;
@Schema(description = "请求参数", example = "userId=1")
private String requestParams;
@Schema(description = "操作时间", example = "2024-01-01 12:00:00")
private LocalDateTime operationTime;
}
审计AOP拦截器
package com.jam.demo.security.audit;
import com.alibaba.fastjson2.JSON;
import com.jam.demo.entity.SensitiveAuditLog;
import com.jam.demo.security.desensitize.DataSecurityContextHolder;
import com.jam.demo.service.SensitiveAuditLogService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 敏感操作审计AOP
* @author ken
*/
@Slf4j
@Aspect
@Component
public class SensitiveAuditAspect {
private final SensitiveAuditLogService auditLogService;
public SensitiveAuditAspect(SensitiveAuditLogService auditLogService) {
this.auditLogService = auditLogService;
}
@Around("@annotation(com.jam.demo.security.audit.SensitiveAudit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
result = joinPoint.proceed();
} finally {
saveAuditLog(joinPoint);
}
return result;
}
private void saveAuditLog(ProceedingJoinPoint joinPoint) {
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SensitiveAudit sensitiveAudit = method.getAnnotation(SensitiveAudit.class);
if (ObjectUtils.isEmpty(sensitiveAudit)) {
return;
}
DataSecurityContextHolder.DataSecurityContext context = DataSecurityContextHolder.getContext();
if (ObjectUtils.isEmpty(context)) {
return;
}
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
SensitiveAuditLog auditLog = new SensitiveAuditLog();
auditLog.setUserId(context.getUserId());
auditLog.setOperation(sensitiveAudit.operation());
auditLog.setDataType(sensitiveAudit.dataType());
auditLog.setOperationIp(request.getRemoteAddr());
auditLog.setRequestMethod(signature.getDeclaringTypeName() + "." + signature.getName());
auditLog.setRequestParams(JSON.toJSONString(joinPoint.getArgs()));
auditLog.setOperationTime(LocalDateTime.now());
auditLogService.save(auditLog);
} catch (Exception e) {
log.error("保存审计日志失败", e);
}
}
}
4.2.2 数据安全合规校验清单
为了确保合规落地,企业需建立常态化的合规校验机制,核心校验项如下:
- 个人信息收集:是否遵循最小必要原则,是否获得用户明确同意,是否有清晰的隐私政策。
- 数据存储:敏感数据是否加密存储,是否有明确的存储期限,是否超期存储。
- 数据传输:数据传输是否采用TLS1.2及以上加密协议,是否存在明文传输敏感数据的情况。
- 数据使用:是否超出用户同意的范围使用数据,是否对敏感数据进行脱敏处理。
- 数据共享:向第三方提供数据是否获得用户单独同意,是否对第三方进行安全评估,是否签订数据处理协议。
- 数据销毁:过期数据是否进行安全销毁,是否存在可恢复的风险。
- 安全防护:是否有完善的安全防护措施,是否定期进行安全漏洞扫描与渗透测试。
- 审计追溯:是否有完整的审计日志,日志留存时间是否符合合规要求。
- 应急处置:是否有数据安全事件应急预案,是否定期进行应急演练。
五、数据安全的最佳实践与避坑指南
5.1 核心最佳实践
- 安全左移:将数据安全融入业务开发的全流程,在需求评审、架构设计、代码开发、测试上线的每个环节,都进行安全评审与校验,避免上线后再补安全漏洞。
- 密钥统一管理:通过专业的KMS密钥管理系统统一管理加密密钥,禁止在代码、配置文件中硬编码密钥,密钥需定期轮换,泄露后可快速吊销与更换。
- 最小权限原则:所有系统、账号、人员的权限,都设置为完成业务所需的最小权限,禁止过度授权,定期进行权限审计与清理。
- 全链路加密:实现数据传输加密、存储加密、使用加密的全链路加密防护,敏感数据全程不落地明文。
- 常态化安全运营:建立常态化的安全运营机制,定期进行安全扫描、渗透测试、合规审计、应急演练,持续优化数据安全防护体系。
5.2 常见避坑指南
- 加密算法使用不当
- 错误做法:使用ECB模式的AES、PKCS#1 v1.5填充的RSA、MD5/SHA-1等已破解的算法,固定IV值,密钥长度不足。
- 正确做法:使用GCM模式的AES-256、OAEP填充的RSA-2048+、SM系列国密算法,每次加密使用随机IV,密钥长度符合安全标准。
- 密钥管理混乱
- 错误做法:密钥硬编码在代码、配置文件中,密钥多人持有,长期不轮换,密钥与加密数据存储在一起。
- 正确做法:使用KMS系统统一管理密钥,密钥分权管控,定期轮换,密钥与加密数据物理隔离存储。
- 密码存储不安全
- 错误做法:直接存储明文密码,使用MD5/SHA-256等快速哈希算法存储密码,固定盐值。
- 正确做法:使用BCrypt、PBKDF2、Argon2等慢哈希算法存储密码,每次加密使用随机盐值,禁止存储可解密的密码密文。
- 脱敏不彻底
- 错误做法:测试环境直接使用生产的明文敏感数据,仅在前端展示脱敏,后端接口返回完整明文数据,管理员权限无管控。
- 正确做法:测试环境使用静态脱敏后的数据,后端接口与数据库查询层双层脱敏,管理员权限需双人授权与审计。
- 合规流于形式
- 错误做法:隐私政策照搬模板,用户同意一揽子授权,无实际的安全防护措施,审计日志造假或缺失。
- 正确做法:隐私政策清晰易懂,用户单独授权,安全措施落地执行,审计日志完整可追溯,定期进行合规校验。
数据安全不是一次性的项目,而是持续的、体系化的运营工作。在数字经济时代,数据安全已经成为企业的核心竞争力,也是企业生存发展的底线。只有构建覆盖加密、脱敏、分级存储、合规的全链路数据安全防护体系,才能真正保护好企业的核心数据资产,规避合规风险,实现可持续的发展。