一、数据库敏感字段级加密实现
档案行业保密认证强制要求敏感数据(如身份证号、涉密等级)必须以密文形式存储。实操中,我们采用AES-256-GCM算法,该算法不仅满足保密性,还提供完整性校验。以下是基于Java Spring Boot环境的具体实现步骤。
在pom.xml中引入必要的加密库依赖:
```xml
org.bouncycastle
bcprov-jdk15on
1.70
```
接着,创建一个工具类AesUtil.java,用于处理加解密逻辑。注意,实际生产环境中密钥严禁硬编码,需通过后续章节的密钥管理服务获取。
```java
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Base64;
public class AesUtil {
private static final String ALGO = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
private static final int AES_KEY_BIT = 256;
public static String encrypt(String plaintext, SecretKey secretKey) throws Exception {
byte[] iv = new byte[IV_LENGTH_BYTE];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes());
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + encryptedBytes.length);
byteBuffer.put(iv);
byteBuffer.put(encryptedBytes);
return Base64.getEncoder().encodeToString(byteBuffer.array());
}
public static String decrypt(String ciphertext, SecretKey secretKey) throws Exception {
byte[] decoded = Base64.getDecoder().decode(ciphertext);
ByteBuffer byteBuffer = ByteBuffer.wrap(decoded);
byte[] iv = new byte[IV_LENGTH_BYTE];
byteBuffer.get(iv);
byte[] encryptedBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(encryptedBytes);
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
return new String(cipher.doFinal(encryptedBytes));
}
}
```
在MyBatis Plus中,我们需要自定义一个TypeHandler来实现实体类字段与数据库密文之间的自动转换。创建CryptoTypeHandler.java:
```java
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@MappedTypes(String.class)
public class CryptoTypeHandler extends BaseTypeHandler
{
// 假设 SecretKeyManager 是获取密钥的单例类
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
try {
String encrypted = AesUtil.encrypt(parameter, SecretKeyManager.getKey());
ps.setString(i, encrypted);
} catch (Exception e) {
throw new SQLException("Encryption failed", 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 (value == null) return null;
try {
return AesUtil.decrypt(value, SecretKeyManager.getKey());
} catch (Exception e) {
throw new RuntimeException("Decryption failed", e);
}
}
}
```
在实体类中,对需要加密的字段添加注解:
```java
@TableField(typeHandler = CryptoTypeHandler.class)
private String idCardNo;
```
二、传输层强制HTTPS与TLS配置
保密认证明确禁止明文传输。我们需要配置Nginx反向代理,强制使用HTTPS,并禁用弱加密套件。以下是基于Nginx的配置方案。
使用OpenSSL生成高强度证书(测试环境可自签,生产环境需购买正规CA证书):
```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/archive.key \
-out /etc/nginx/ssl/archive.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=ArchiveTech/OU=IT/CN=archive.local"
```
编辑Nginx配置文件/etc/nginx/conf.d/archive.conf,核心在于配置ssl_protocols和ssl_ciphers:
```nginx
server {
listen 80;
server_name archive.local;
强制跳转HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name archive.local;
ssl_certificate /etc/nginx/ssl/archive.crt;
ssl_certificate_key /etc/nginx/ssl/archive.key;
仅启用TLS 1.2和1.3,禁用SSLv2, SSLv3, TLS 1.0, TLS 1.1
ssl_protocols TLSv1.2 TLSv1.3;
优先使用服务器端的加密套件顺序
ssl_prefer_server_ciphers on;
配置高强度加密套件,符合等保及保密认证要求
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384';
开启HSTS,强制浏览器使用HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
禁止在响应头中泄露Nginx版本号
server_tokens off;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
配置完成后,执行nginx -t检查语法,无误后执行systemctl restart nginx生效。你可以使用openssl s_client -connect archive.local:443 -tls1_2命令验证连接是否使用了AES256-GCM等强加密算法。
三、基于Spring Security的“三权分立”权限控制
档案行业保密认证核心要求之一是“三权分立”,即系统管理员、安全保密员、安全审计员权限必须互斥,一人不得身兼数职。我们需要在Spring Security中实现严格的RBAC模型。

定义角色常量:
```java
public class SystemRoles {
public static final String ADMIN = "ROLE_ADMIN"; // 系统管理员:负责系统维护
public static final String SECURITY_OFFICER = "ROLE_SEC_ADMIN"; // 安全保密员:负责用户授权、策略制定
public static final String AUDITOR = "ROLE_AUDITOR"; // 安全审计员:负责查阅审计日志
}
```
配置SecurityConfig.java,细化URL访问控制。注意,审计员只能访问日志接口,不能访问用户管理接口;保密员不能修改系统配置。
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 系统管理员:系统配置、服务器监控
.antMatchers("/api/system/config/", "/api/actuator/").hasRole(SystemRoles.ADMIN)
// 安全保密员:用户管理、角色分配(但不能分配给自己)
.antMatchers("/api/user/", "/api/role/").hasRole(SystemRoles.SECURITY_OFFICER)
// 安全审计员:仅能查看日志,严禁拥有任何写操作权限
.antMatchers("/api/audit/").hasRole(SystemRoles.AUDITOR)
// 档案业务接口:需要登录用户
.antMatchers("/api/archive/").authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/index")
.and()
.logout()
.logoutSuccessUrl("/login")
.and()
// 防止CSRF攻击,如果是前后端分离需配置CorsFilter
.csrf().disable();
}
}
```
为了防止权限越权,需在Service层增加校验逻辑。例如,在创建用户时,检查当前操作者是否为安全保密员:
```java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public User createUser(User user, String operatorRole) {
if (!SystemRoles.SECURITY_OFFICER.equals(operatorRole)) {
throw new AccessDeniedException("只有安全保密员才能创建用户");
}
// 逻辑校验:安全保密员不能创建系统管理员或审计员
if (SystemRoles.ADMIN.equals(user.getRole()) || SystemRoles.AUDITOR.equals(user.getRole())) {
throw new AccessDeniedException("安全保密员无权授予系统管理员或审计员权限");
}
return userRepository.save(user);
}
}
```
四、全量操作审计日志系统
保密认证要求所有用户登录、操作、数据导出行为必须留存记录,且日志需防篡改。我们使用Spring AOP切面实现自动日志记录,并写入独立的审计日志表。
定义审计日志注解@AuditLog:
```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
String operation() default ""; // 操作描述,如 "删除档案"
String module() default ""; // 模块名
}
```
实现AOP切面AuditLogAspect.java:
```java
@Aspect
@Component
public class AuditLogAspect {
@Autowired
private AuditLogRepository auditLogRepository;
@Autowired
private HttpServletRequest request;
// 定义切点,扫描Service层
@Pointcut("execution( com.yourpackage.service..(..)) && @annotation(auditLog)")
public void logPointCut(AuditLog auditLog) {}
@Around("logPointCut(auditLog)")
public Object around(ProceedingJoinPoint point, AuditLog auditLog) throws Throwable {
long beginTime = System.currentTimeMillis();
Object result = point.proceed();
long costTime = System.currentTimeMillis() - beginTime;
// 获取当前用户信息
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String ip = getIpAddr(request);
// 构建日志实体
SysAuditLog log = new SysAuditLog();
log.setUsername(username);
log.setIp(ip);
log.setModule(auditLog.module());
log.setOperation(auditLog.operation());
log.setParams(Arrays.toString(point.getArgs())); // 实际中需对敏感参数脱敏
log.setTime(costTime);
log.setCreateTime(new Date());
// 异步保存日志,防止影响业务性能
auditLogRepository.save(log);
return result;
}
private String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
```
在业务方法上应用注解:
```java
@Service
public class ArchiveService {
@AuditLog(operation = "删除涉密档案", module = "档案管理")
public void deleteArchive(Long id) {
// 删除逻辑
}
}
```
五、密钥安全存储与轮换
绝对禁止将数据库加密密钥存储在代码或配置文件中。我们使用Java KeyStore(JKS)将密钥存储在服务器本地文件中,并设置强口令保护。
1. 生成KeyStore文件并存储AES密钥:
```bash
keytool -importkeystore -deststorepass changeit -destkeypass changeit -destkeystore archive-keystore.jks -srckeystore archive.p12 -srcstoretype PKCS12 -srcstorepass changeit -alias archive-aes-key
```
2. 创建SecretKeyManager.java单例类,用于在启动时加载密钥:
```java
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.util.Base64;
public class SecretKeyManager {
private static SecretKey key;
public static synchronized SecretKey getKey() {
if (key != null) return key;
try {
// 密钥库路径和密码(实际应从环境变量读取)
String keystorePath = "/etc/secrets/archive-keystore.jks";
String keystorePassword = System.getenv("KEYSTORE_PASS");
FileInputStream is = new FileInputStream(keystorePath);
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(is, keystorePassword.toCharArray());
KeyStore.SecretKeyEntry ske = (KeyStore.SecretKeyEntry) ks.getEntry("archive-aes-key",
new KeyStore.PasswordProtection(keystorePassword.toCharArray()));
key = ske.getSecretKey();
return key;
} catch (Exception e) {
throw new RuntimeException("Failed to load secret key", e);
}
}
}
```
3. 密钥轮换策略:建议每季度进行一次密钥轮换。轮换时,使用旧密钥解密所有数据,再用新密钥加密并回写数据库,随后更新KeyStore文件。此过程需编写离线批处理脚本执行,避免在线业务高峰期操作。