点狮HRM-HRM系统安全体系与数据保护方案

引言

在当今数字化时代,企业人力资源管理(HRM)系统承载着员工最敏感的个人信息------薪资数据、家庭住址、身份证号、银行卡信息等。一旦这些数据泄露,不仅会造成严重的经济损失和声誉损害,还可能面临法律合规风险。根据《2024年数据泄露成本报告》,人力资源数据泄露的平均成本高达472万美元,远高于其他行业平均水平。

本文将深入探讨如何构建一个企业级HRM系统的安全体系,从数据加密、权限控制、审计追踪到合规性保障,全方位保护HR敏感数据。我们将结合实际代码实现,展示如何在Spring Cloud Alibaba微服务架构中,为HRM系统构建一套完整的安全防护体系。


相关链接

一、HRM系统安全威胁分析

1.1 核心威胁识别

数据泄露威胁

  • 薪资数据泄露:员工薪资信息被非法获取,引发内部矛盾和法律风险
  • 个人信息泄露:身份证号、手机号、家庭住址等PII数据外泄
  • 考勤数据泄露:员工行为模式被恶意分析

权限滥用威胁

  • HR管理员权限过大:可查看/修改所有员工数据
  • 越权访问:普通员工访问管理层薪资信息
  • 数据导出滥用:批量导出敏感数据

系统攻击威胁

  • SQL注入攻击:恶意查询薪资数据
  • API滥用:自动化脚本批量爬取数据
  • 中间人攻击:拦截传输中的敏感数据

1.2 法规合规要求

国内法规

  • 《个人信息保护法》:要求对敏感个人信息实行重点保护
  • 《数据安全法》:要求数据分类分级保护
  • 《网络安全法》:要求数据加密存储和传输

国际标准

  • GDPR:要求数据最小化原则和隐私保护
  • ISO 27001:信息安全管理体系标准
  • SOC 2:服务组织控制报告

1.3 HRM系统特殊安全需求

数据敏感性分级

数据级别 数据类型 保护要求
L1-绝密 CEO及高管薪资、股权激励 AES-256加密+双因素认证
L2-机密 普通员工薪资、绩效数据 AES-256加密+角色访问控制
L3-敏感 身份证号、银行卡号 AES-256加密+脱敏展示
L4-内部 考勤记录、请假记录 RBAC权限控制
L5-公开 组织架构、联系方式 基础访问控制

访问控制特殊性

  • 薪资数据的"最小知悉原则":只有授权HR和员工本人可查看
  • 绩效数据的"分级访问":主管可查看下属,员工查看自己
  • 考勤数据的"部门隔离":部门主管只能查看本部门数据

二、数据加密方案设计与实现

2.1 整体加密架构

分层加密策略

复制代码
┌─────────────────────────────────────────────────────────┐
│                   应用层加密                              │
│  - 敏感字段加密(AES-256)                                │
│  - 数据库透明加密(TDE)                                  │
│  - 文件存储加密                                           │
├─────────────────────────────────────────────────────────┤
│                   传输层加密                              │
│  - HTTPS/TLS 1.3                                         │
│  - 内部服务间mTLS                                         │
│  - API签名验证                                           │
├─────────────────────────────────────────────────────────┤
│                   密钥管理层                             │
│  - 密钥 rotation策略                                     │
│  - HSM硬件安全模块                                       │
│  - 密钥分片存储                                          │
└─────────────────────────────────────────────────────────┘

2.2 敏感数据字段级加密

加密服务实现

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.pointlion.cloud.module.hrm.service.security.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 敏感数据加密服务
 *
 * 功能特性:
 * 1. AES-256-GCM加密算法
 * 2. 每条记录独立IV(初始化向量)
 * 3. 密钥定期轮换
 * 4. 加密性能优化(缓存+批量处理)
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class DataEncryptionService {

    @Resource
    private SecurityProperties securityProperties;

    /**
     * 加密器缓存(按密钥版本分类)
     */
    private final Map<String, AES> encryptorCache = new ConcurrentHashMap<>();

    /**
     * 初始化BouncyCastle加密提供者
     */
    @PostConstruct
    public void init() {
        Security.addProvider(new BouncyCastleProvider());
        log.info("数据加密服务初始化完成");
    }

    /**
     * 加密敏感字段
     *
     * @param plainText 明文
     * @param fieldType 字段类型(用于选择加密密钥)
     * @return 密文(格式:base64(iv+ciphertext))
     */
    public String encryptField(String plainText, FieldType fieldType) {
        if (plainText == null || plainText.isEmpty()) {
            return plainText;
        }

        try {
            // 1. 获取对应字段的加密密钥
            String encryptionKey = getEncryptionKey(fieldType);
            AES aes = getEncryptor(encryptionKey);

            // 2. 生成随机IV(重要:每次加密使用不同IV)
            byte[] iv = SecureUtil.createIV("AES", 128);

            // 3. AES-256-GCM加密
            byte[] encrypted = aes.encrypt(plainText.getBytes(StandardCharsets.UTF_8));

            // 4. 组合IV+密文并Base64编码
            byte[] combined = new byte[iv.length + encrypted.length];
            System.arraycopy(iv, 0, combined, 0, iv.length);
            System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);

            return Base64.getEncoder().encodeToString(combined);

        } catch (Exception e) {
            log.error("字段加密失败: fieldType={}, plainText={}",
                    fieldType, maskSensitive(plainText), e);
            throw new SecurityException("数据加密失败", e);
        }
    }

    /**
     * 解密敏感字段
     *
     * @param cipherText 密文(格式:base64(iv+ciphertext))
     * @param fieldType 字段类型
     * @return 明文
     */
    public String decryptField(String cipherText, FieldType fieldType) {
        if (cipherText == null || cipherText.isEmpty()) {
            return cipherText;
        }

        try {
            // 1. Base64解码
            byte[] combined = Base64.getDecoder().decode(cipherText);

            // 2. 分离IV和密文(前16字节是IV)
            byte[] iv = new byte[16];
            byte[] encrypted = new byte[combined.length - 16];
            System.arraycopy(combined, 0, iv, 0, 16);
            System.arraycopy(combined, 16, encrypted, 0, encrypted.length);

            // 3. 获取解密器
            String encryptionKey = getEncryptionKey(fieldType);
            AES aes = getEncryptor(encryptionKey);

            // 4. 解密
            byte[] decrypted = aes.decrypt(encrypted);

            return new String(decrypted, StandardCharsets.UTF_8);

        } catch (Exception e) {
            log.error("字段解密失败: fieldType={}, cipherText={}",
                    fieldType, maskSensitive(cipherText), e);
            throw new SecurityException("数据解密失败", e);
        }
    }

    /**
     * 批量加密(性能优化)
     *
     * @param plainTexts 明文列表
     * @param fieldType 字段类型
     * @return 密文列表
     */
    public Map<String, String> encryptBatch(Map<String, String> plainTexts, FieldType fieldType) {
        Map<String, String> encryptedMap = new ConcurrentHashMap<>();

        plainTexts parallelStream().forEach(entry -> {
            String encrypted = encryptField(entry.getValue(), fieldType);
            encryptedMap.put(entry.getKey(), encrypted);
        });

        return encryptedMap;
    }

    /**
     * 批量解密(性能优化)
     */
    public Map<String, String> decryptBatch(Map<String, String> cipherTexts, FieldType fieldType) {
        Map<String, String> decryptedMap = new ConcurrentHashMap<>();

        cipherTexts parallelStream().forEach(entry -> {
            String decrypted = decryptField(entry.getValue(), fieldType);
            decryptedMap.put(entry.getKey(), decrypted);
        });

        return decryptedMap;
    }

    /**
     * 获取加密密钥(支持密钥版本和轮换)
     */
    private String getEncryptionKey(FieldType fieldType) {
        // 根据字段类型获取对应的密钥
        // 支持密钥版本控制,便于密钥轮换
        return switch (fieldType) {
            case ID_CARD -> securityProperties.getIdCardEncryptionKey();
            case BANK_CARD -> securityProperties.getBankCardEncryptionKey();
            case SALARY -> securityProperties.getSalaryEncryptionKey();
            case PHONE -> securityProperties.getPhoneEncryptionKey();
            default -> securityProperties.getDefaultEncryptionKey();
        };
    }

    /**
     * 获取加密器(带缓存)
     */
    private AES getEncryptor(String encryptionKey) {
        return encryptorCache computeIfAbsent(encryptionKey, key -> {
            // 使用AES-256-GCM模式(带认证的加密)
            return SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8));
        });
    }

    /**
     * 敏感信息脱敏(用于日志输出)
     */
    private String maskSensitive(String text) {
        if (text == null || text.length() <= 4) {
            return "****";
        }
        return text.substring(0, 2) + "****" + text.substring(text.length() - 2);
    }

    /**
     * 字段类型枚举
     */
    public enum FieldType {
        /**
         * 身份证号
         */
        ID_CARD,

        /**
         * 银行卡号
         */
        BANK_CARD,

        /**
         * 薪资数据
         */
        SALARY,

        /**
         * 手机号
         */
        PHONE,

        /**
         * 地址
         */
        ADDRESS
    }
}

安全配置属性

java 复制代码
package com.pointlion.cloud.module.hrm.service.security.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 安全配置属性
 *
 * @author 点狮信息
 */
@Data
@Component
@ConfigurationProperties(prefix = "hrm.security")
public class SecurityProperties {

    /**
     * 数据加密配置
     */
    private Encryption encryption = new Encryption();

    /**
     * 数据脱敏配置
     */
    private Masking masking = new Masking();

    /**
     * 审计配置
     */
    private Audit audit = new Audit();

    @Data
    public static class Encryption {
        /**
         * 默认加密密钥(32字节=256位,生产环境应从KMS获取)
         */
        private String defaultEncryptionKey;

        /**
         * 身份证号加密密钥
         */
        private String idCardEncryptionKey;

        /**
         * 银行卡号加密密钥
         */
        private String bankCardEncryptionKey;

        /**
         * 薪资数据加密密钥
         */
        private String salaryEncryptionKey;

        /**
         * 手机号加密密钥
         */
        private String phoneEncryptionKey;

        /**
         * 密钥版本(用于密钥轮换)
         */
        private Integer keyVersion = 1;
    }

    @Data
    public static class Masking {
        /**
         * 是否启用脱敏
         */
        private Boolean enabled = true;

        /**
         * 身份证号脱敏规则
         */
        private String idCardMaskRule = "(\\d{6})\\d{8}(\\d{4})";

        /**
         * 手机号脱敏规则
         */
        private String phoneMaskRule = "(\\d{3})\\d{4}(\\d{4})";
    }

    @Data
    public static class Audit {
        /**
         * 是否启用审计
         */
        private Boolean enabled = true;

        /**
         * 审计日志保留天数
         */
        private Integer retentionDays = 365;

        /**
         * 是否记录敏感数据访问
         */
        private Boolean logSensitiveAccess = true;
    }
}

2.3 数据传输加密

HTTPS配置

yaml 复制代码
# application.yml
server:
  port: 8080
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: ${SSL_KEY_STORE_PASSWORD}
    key-store-type: PKCS12
    key-alias: pointlion-hrm
    protocol: TLSv1.3
    ciphers:
      - TLS_AES_256_GCM_SHA384
      - TLS_CHACHA20_POLY1305_SHA256
      - TLS_AES_128_GCM_SHA256

内部服务间mTLS配置

java 复制代码
package com.pointlion.cloud.module.hrm.service.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.*;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

/**
 * 服务间通信安全配置
 *
 * 实现双向TLS认证(mTLS),确保微服务间通信安全
 *
 * @author 点狮信息
 */
@Configuration
public class ServiceSecurityConfig {

    /**
     * 配置支持mTLS的RestTemplate
     */
    @Bean
    public RestTemplate secureRestTemplate() throws Exception {
        // 加载服务端证书
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (InputStream is = getClass().getResourceAsStream("/service.p12")) {
            keyStore.load(is, "changeit".toCharArray());
        }

        // 加载信任库
        KeyStore trustStore = KeyStore.getInstance("PKCS12");
        try (InputStream is = getClass().getResourceAsStream("/truststore.p12")) {
            trustStore.load(is, "changeit".toCharArray());
        }

        // 配置SSL上下文
        SSLContext sslContext = SSLContext.getInstance("TLS");

        KeyManagerFactory kmf = KeyManagerFactory.getInstance(
                KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(keyStore, "changeit".toCharArray());

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);

        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());

        // 配置OkHttpClient
        okhttp3.OkHttpClient okHttpClient = new okhttp3.OkHttpClient.Builder()
                .sslSocketFactory(sslContext.getSocketFactory(),
                        (X509TrustManager) tmf.getTrustManagers()[0])
                .hostnameVerifier((hostname, session) -> {
                    // 生产环境应严格验证主机名
                    // 开发环境可以放宽
                    return true;
                })
                .build();

        OkHttp3ClientHttpRequestFactory requestFactory =
                new OkHttp3ClientHttpRequestFactory(okHttpClient);

        return new RestTemplate(requestFactory);
    }
}

2.4 密钥管理方案

密钥轮换策略

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import com.pointlion.cloud.module.hrm.service.security.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 密钥轮换服务
 *
 * 定期轮换加密密钥,降低密钥泄露风险
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class KeyRotationService {

    @Resource
    private SecurityProperties securityProperties;

    /**
     * 密钥版本映射(版本号 -> 密钥)
     */
    private final Map<Integer, String> keyVersionMap = new ConcurrentHashMap<>();

    /**
     * 当前密钥版本
     */
    private volatile Integer currentVersion = 1;

    /**
     * 密钥轮换周期(天)
     */
    private static final int ROTATION_DAYS = 90;

    /**
     * 上次轮换时间
     */
    private volatile Instant lastRotationTime;

    /**
     * 初始化密钥版本
     */
    public void initializeKeyVersions() {
        keyVersionMap.put(1, securityProperties.getEncryption().getDefaultEncryptionKey());
        keyVersionMap.put(2, securityProperties.getEncryption().getSalaryEncryptionKey());
        lastRotationTime = Instant.now();
    }

    /**
     * 定期检查并执行密钥轮换(每周检查一次)
     */
    @Scheduled(cron = "0 0 2 * * MON")
    public void checkAndRotateKeys() {
        Instant now = Instant.now();
        long daysSinceRotation = ChronoUnit.DAYS.between(lastRotationTime, now);

        if (daysSinceRotation >= ROTATION_DAYS) {
            log.info("执行密钥轮换,上次轮换时间:{},已过{}天",
                    lastRotationTime, daysSinceRotation);
            rotateKeys();
        }
    }

    /**
     * 执行密钥轮换
     */
    private synchronized void rotateKeys() {
        try {
            // 1. 生成新密钥(实际应从KMS获取)
            Integer newVersion = currentVersion + 1;
            String newKey = generateNewKey();

            // 2. 将新密钥加入版本映射
            keyVersionMap.put(newVersion, newKey);

            // 3. 标记新密钥为当前版本
            currentVersion = newVersion;

            // 4. 更新轮换时间
            lastRotationTime = Instant.now();

            log.info("密钥轮换成功,新版本:{}", newVersion);

            // 5. 异步触发旧数据重加密(可选)
            triggerReEncryption();

        } catch (Exception e) {
            log.error("密钥轮换失败", e);
        }
    }

    /**
     * 生成新密钥(生产环境应从KMS获取)
     */
    private String generateNewKey() {
        // 实际应从AWS KMS、阿里云KMS等获取
        // 这里简化为32字节随机密钥
        byte[] keyBytes = new byte[32];
        new java.security.SecureRandom().nextBytes(keyBytes);
        return java.util.Base64.getEncoder().encodeToString(keyBytes);
    }

    /**
     * 获取指定版本的密钥
     */
    public String getKeyByVersion(Integer version) {
        return keyVersionMap.get(version);
    }

    /**
     * 获取当前密钥版本
     */
    public Integer getCurrentVersion() {
        return currentVersion;
    }

    /**
     * 触发旧数据重加密(异步任务)
     */
    private void triggerReEncryption() {
        // 提交到异步线程池,逐步重加密旧数据
        // 避免阻塞主流程
    }
}

三、数据脱敏与访问控制

3.1 数据脱敏服务

脱敏服务实现

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import cn.hutool.core.util.DesensitizedUtil;
import com.pointlion.cloud.module.hrm.service.security.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.function.Function;

/**
 * 数据脱敏服务
 *
 * 对敏感数据进行脱敏处理,防止泄露
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class DataMaskingService {

    @Resource
    private SecurityProperties securityProperties;

    /**
     * 根据用户权限脱敏员工数据
     *
     * @param employeeDO 员工数据
     * @param hasFullAccess 是否有完整数据访问权限
     * @return 脱敏后的数据
     */
    public <T> T maskEmployeeData(T employeeDO, boolean hasFullAccess) {
        if (hasFullAccess) {
            // 有完整权限,不脱敏
            return employeeDO;
        }

        // 无完整权限,根据字段类型脱敏
        return maskSensitiveFields(employeeDO);
    }

    /**
     * 脱敏敏感字段
     */
    private <T> T maskSensitiveFields(T data) {
        // 使用反射或自定义逻辑脱敏各个字段
        // 这里简化处理

        if (data instanceof EmployeeDTO) {
            EmployeeDTO dto = (EmployeeDTO) data;
            dto.setIdCard(maskIdCard(dto.getIdCard()));
            dto.setPhone(maskPhone(dto.getPhone()));
            dto.setBankCard(maskBankCard(dto.getBankCard()));
            dto.setAddress(maskAddress(dto.getAddress()));
            return data;
        }

        return data;
    }

    /**
     * 脱敏身份证号
     * 保留前6位和后4位
     */
    public String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() != 18) {
            return idCard;
        }
        return DesensitizedUtil.idCardNum(idCard, 6, 4);
    }

    /**
     * 脱敏手机号
     * 保留前3位和后4位
     */
    public String maskPhone(String phone) {
        if (phone == null || phone.length() != 11) {
            return phone;
        }
        return DesensitizedUtil.mobilePhone(phone);
    }

    /**
     * 脱敏银行卡号
     * 保留前4位和后4位
     */
    public String maskBankCard(String bankCard) {
        if (bankCard == null || bankCard.length() < 8) {
            return bankCard;
        }
        return DesensitizedUtil.bankCard(bankCard);
    }

    /**
     * 脱敏地址
     * 只显示到区/县级
     */
    public String maskAddress(String address) {
        if (address == null || address.isEmpty()) {
            return address;
        }
        // 保留前10个字符(大致到区/县级)
        if (address.length() <= 10) {
            return address + "****";
        }
        return address.substring(0, 10) + "****";
    }

    /**
     * 脱敏薪资数据
     * 显示为范围
     */
    public String maskSalary(Integer salary) {
        if (salary == null) {
            return null;
        }
        // 薪资显示为5k-10k的形式
        int range = (salary / 1000) / 5 * 5;
        return range + "k-" + (range + 5) + "k";
    }

    /**
     * 根据权限判断是否可以访问原始数据
     *
     * @param fieldType 字段类型
     * @param permission 用户权限
     * @return true-可以访问原始数据
     */
    public boolean canAccessRawData(FieldType fieldType, String permission) {
        return switch (fieldType) {
            case SALARY -> permission.contains("salary:view");
            case ID_CARD, BANK_CARD -> permission.contains("personal:view:full");
            case PHONE -> permission.contains("personal:view");
            default -> true;
        };
    }

    /**
     * 字段类型枚举
     */
    public enum FieldType {
        SALARY, ID_CARD, BANK_CARD, PHONE, ADDRESS
    }
}

3.2 权限控制模型

数据权限枚举

java 复制代码
package com.pointlion.cloud.module.hrm.service.security.enums;

import lombok.Getter;

/**
 * HRM数据权限类型
 *
 * @author 点狮信息
 */
@Getter
public enum DataPermissionType {

    /**
     * 仅本人
     */
    SELF(1, "仅本人"),

    /**
     * 本部门
     */
    DEPARTMENT(2, "本部门"),

    /**
     * 本部门及子部门
     */
    DEPARTMENT_AND_SUB(3, "本部门及子部门"),

    /**
     * 全公司
     */
    ALL(4, "全公司"),

    /**
     * 指定部门
     */
    SPECIFIED_DEPARTMENT(5, "指定部门"),

    /**
     * 自定义
     */
    CUSTOM(6, "自定义");

    private final Integer code;
    private final String description;

    DataPermissionType(Integer code, String description) {
        this.code = code;
        this.description = description;
    }

    public static DataPermissionType fromCode(Integer code) {
        for (DataPermissionType type : values()) {
            if (type.code.equals(code)) {
                return type;
            }
        }
        return SELF; // 默认仅本人
    }
}

权限检查服务

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import com.pointlion.cloud.module.hrm.dal.dataobject.employee.EmployeeDO;
import com.pointlion.cloud.module.hrm.service.security.enums.DataPermissionType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Set;

/**
 * 数据权限检查服务
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class DataPermissionService {

    /**
     * 检查是否有权限访问目标员工数据
     *
     * @param currentUser 当前用户
     * @param targetUserId 目标用户ID
     * @param permissionType 权限类型
     * @return true-有权限
     */
    public boolean hasPermission(EmployeeDO currentUser,
                                   Long targetUserId,
                                   DataPermissionType permissionType) {

        // 1. 本人可以访问自己的数据
        if (currentUser.getId().equals(targetUserId)) {
            return true;
        }

        // 2. 根据权限类型判断
        return switch (permissionType) {
            case SELF -> false; // 只能访问自己,已在上一步处理

            case DEPARTMENT -> {
                // 只能访问本部门数据
                yield currentUser.getDepartmentId().equals(
                        getDepartmentIdByUserId(targetUserId));
            }

            case DEPARTMENT_AND_SUB -> {
                // 可以访问本部门及子部门数据
                Long currentDeptId = currentUser.getDepartmentId();
                Long targetDeptId = getDepartmentIdByUserId(targetUserId);
                yield isDepartmentOrSub(currentDeptId, targetDeptId);
            }

            case ALL -> true; // 可以访问全公司数据

            case SPECIFIED_DEPARTMENT -> {
                // 可以访问指定部门数据
                Set<Long> specifiedDepts = currentUser.getSpecifiedDepartments();
                Long targetDeptId = getDepartmentIdByUserId(targetUserId);
                yield specifiedDepts != null && specifiedDepts.contains(targetDeptId);
            }

            case CUSTOM -> {
                // 自定义权限逻辑
                yield checkCustomPermission(currentUser, targetUserId);
            }
        };
    }

    /**
     * 检查是否是部门或子部门
     */
    private boolean isDepartmentOrSub(Long parentDeptId, Long childDeptId) {
        if (parentDeptId.equals(childDeptId)) {
            return true;
        }
        // 检查childDeptId是否是parentDeptId的子部门
        List<Long> subDeptIds = getSubDepartmentIds(parentDeptId);
        return subDeptIds.contains(childDeptId);
    }

    /**
     * 获取用户所属部门ID
     */
    private Long getDepartmentIdByUserId(Long userId) {
        // 从数据库或缓存获取
        return null;
    }

    /**
     * 获取子部门ID列表
     */
    private List<Long> getSubDepartmentIds(Long departmentId) {
        // 从数据库或缓存获取
        return null;
    }

    /**
     * 自定义权限检查
     */
    private boolean checkCustomPermission(EmployeeDO currentUser, Long targetUserId) {
        // 实现自定义权限逻辑
        // 例如:只能访问直接下属
        return isDirectSubordinate(currentUser.getId(), targetUserId);
    }

    /**
     * 检查是否是直接下属
     */
    private boolean isDirectSubordinate(Long managerId, Long userId) {
        // 检查userId的直接上级是否是managerId
        return false;
    }

    /**
     * 过滤可访问的用户ID列表
     *
     * @param currentUser 当前用户
     * @param permissionType 权限类型
     * @param allUserIds 所有用户ID
     * @return 可访问的用户ID列表
     */
    public List<Long> filterAccessibleUsers(EmployeeDO currentUser,
                                            DataPermissionType permissionType,
                                            List<Long> allUserIds) {
        return allUserIds.stream()
                .filter(userId -> hasPermission(currentUser, userId, permissionType))
                .toList();
    }
}

3.3 薪资数据特殊访问控制

薪资访问控制服务

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import com.pointlion.cloud.module.hrm.dal.dataobject.employee.EmployeeDO;
import com.pointlion.cloud.module.hrm.service.security.enums.DataPermissionType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

/**
 * 薪资数据访问控制服务
 *
 * 薪资数据是HRM系统中最敏感的数据,需要特殊保护
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class SalaryAccessControlService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 薪资查看次数限制(每人每月)
     */
    private static final int MAX_SALARY_VIEW_COUNT_PER_MONTH = 5;

    /**
     * 薪资查看冷却时间(秒)
     */
    private static final int SALARY_VIEW_COOLDOWN_SECONDS = 30;

    /**
     * 检查是否可以查看薪资
     *
     * @param userId 用户ID
     * @param targetUserId 目标用户ID(查看他人薪资时需要)
     * @return true-可以查看
     */
    public boolean canViewSalary(Long userId, Long targetUserId) {
        // 1. 查看自己的薪资总是允许
        if (userId.equals(targetUserId)) {
            return true;
        }

        // 2. 检查是否是授权人员(HR、上级等)
        if (!isAuthorizedPerson(userId, targetUserId)) {
            log.warn("用户{}无权查看用户{}的薪资", userId, targetUserId);
            return false;
        }

        // 3. 检查查看次数限制
        String countKey = getSalaryViewCountKey(userId);
        Long count = stringRedisTemplate.opsForValue().increment(countKey);

        if (count == null) {
            count = 0L;
        }

        // 设置过期时间为月末
        long secondsUntilEndOfMonth = getSecondsUntilEndOfMonth();
        stringRedisTemplate.expire(countKey, secondsUntilEndOfMonth, TimeUnit.SECONDS);

        if (count > MAX_SALARY_VIEW_COUNT_PER_MONTH) {
            log.warn("用户{}本月薪资查看次数已达上限({}次)",
                    userId, MAX_SALARY_VIEW_COUNT_PER_MONTH);
            return false;
        }

        // 4. 检查冷却时间
        String cooldownKey = getSalaryViewCooldownKey(userId);
        Boolean isCoolingDown = stringRedisTemplate.hasKey(cooldownKey);

        if (Boolean.TRUE.equals(isCoolingDown)) {
            Long ttl = stringRedisTemplate.getExpire(cooldownKey, TimeUnit.SECONDS);
            log.warn("用户{}薪资查看冷却中,剩余{}秒", userId, ttl);
            return false;
        }

        // 5. 设置冷却时间
        stringRedisTemplate.opsForValue().set(
                cooldownKey,
                "1",
                SALARY_VIEW_COOLDOWN_SECONDS,
                TimeUnit.SECONDS
        );

        return true;
    }

    /**
     * 检查是否是授权人员
     */
    private boolean isAuthorizedPerson(Long userId, Long targetUserId) {
        // 1. HR人员
        if (isHRUser(userId)) {
            return true;
        }

        // 2. 直属上级
        if (isDirectManager(userId, targetUserId)) {
            return true;
        }

        // 3. 薪资核算人员
        if (isSalaryAccountant(userId)) {
            return true;
        }

        return false;
    }

    /**
     * 检查是否是HR用户
     */
    private boolean isHRUser(Long userId) {
        // 检查用户是否属于HR部门
        return false;
    }

    /**
     * 检查是否是直属上级
     */
    private boolean isDirectManager(Long managerId, Long userId) {
        // 检查用户的直接上级是否是managerId
        return false;
    }

    /**
     * 检查是否是薪资核算人员
     */
    private boolean isSalaryAccountant(Long userId) {
        // 检查用户是否有薪资核算权限
        return false;
    }

    /**
     * 记录薪资查看行为(用于审计)
     */
    public void recordSalaryView(Long userId, Long targetUserId, String reason) {
        // 记录到审计日志
        String logMessage = String.format(
                "用户%d查看用户%d的薪资数据,原因:%s",
                userId, targetUserId, reason
        );
        // 保存到审计日志表或发送到日志系统
    }

    /**
     * 获取薪资查看次数Key
     */
    private String getSalaryViewCountKey(Long userId) {
        int yearMonth = java.time.YearMonth.now().toString();
        return "hrm:salary:view:count:" + yearMonth + ":" + userId;
    }

    /**
     * 获取薪资查看冷却Key
     */
    private String getSalaryViewCooldownKey(Long userId) {
        return "hrm:salary:view:cooldown:" + userId;
    }

    /**
     * 获取到月末的秒数
     */
    private long getSecondsUntilEndOfMonth() {
        java.time.LocalDateTime now = java.time.LocalDateTime.now();
        java.time.LocalDateTime endOfMonth = now
                .withDayOfMonth(now.toLocalDate().lengthOfMonth())
                .withHour(23)
                .withMinute(59)
                .withSecond(59);
        return Duration.between(now, endOfMonth).getSeconds();
    }

    /**
     * 重置薪资查看次数(管理员操作)
     */
    public void resetSalaryViewCount(Long userId) {
        String countKey = getSalaryViewCountKey(userId);
        stringRedisTemplate.delete(countKey);
        log.info("已重置用户{}的薪资查看次数", userId);
    }
}

四、审计日志系统

4.1 审计事件模型

审计事件实体

java 复制代码
package com.pointlion.cloud.module.hrm.dal.dataobject.security;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 审计日志实体
 *
 * @author 点狮信息
 */
@Data
@TableName("hrm_audit_log")
public class AuditLogDO {

    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 事件类型
     * LOGIN, LOGOUT, VIEW_SALARY, EXPORT_DATA, MODIFY_PERMISSION, etc.
     */
    private String eventType;

    /**
     * 操作用户ID
     */
    private Long userId;

    /**
     * 操作用户名
     */
    private String username;

    /**
     * 目标对象类型
     * EMPLOYEE, SALARY, ATTENDANCE, etc.
     */
    private String targetType;

    /**
     * 目标对象ID
     */
    private String targetId;

    /**
     * 操作类型
     * CREATE, READ, UPDATE, DELETE, EXPORT
     */
    private String actionType;

    /**
     * 操作结果
     * SUCCESS, FAILURE
     */
    private String result;

    /**
     * 失败原因
     */
    private String failureReason;

    /**
     * IP地址
     */
    private String ipAddress;

    /**
     * User-Agent
     */
    private String userAgent;

    /**
     * 请求URI
     */
    private String requestUri;

    /**
     * 请求方法
     */
    private String requestMethod;

    /**
     * 请求参数(脱敏后)
     */
    private String requestParams;

    /**
     * 响应数据(脱敏后)
     */
    private String responseData;

    /**
     * 影响的记录数
     */
    private Integer affectedRecords;

    /**
     * 业务模块
     */
    private String module;

    /**
     * 操作描述
     */
    private String description;

    /**
     * 风险等级
     * LOW, MEDIUM, HIGH, CRITICAL
     */
    private String riskLevel;

    /**
     * 是否敏感操作
     */
    private Boolean isSensitive;

    /**
     * 租户ID
     */
    private Long tenantId;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}

4.2 审计日志服务

审计服务实现

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import com.pointlion.cloud.module.hrm.dal.dataobject.security.AuditLogDO;
import com.pointlion.cloud.module.hrm.dal.mysql.security.AuditLogMapper;
import com.pointlion.cloud.module.hrm.service.security.properties.SecurityProperties;
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.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 审计日志服务
 *
 * 记录系统中的重要操作,用于安全审计和合规检查
 *
 * @author 点狮信息
 */
@Slf4j
@Service
@Aspect
public class AuditLogService {

    @Resource
    private AuditLogMapper auditLogMapper;

    @Resource
    private SecurityProperties securityProperties;

    @Resource
    private ThreadPoolExecutor auditLogExecutor;

    /**
     * 记录审计日志(异步)
     */
    @Async("auditLogExecutor")
    public void recordAuditLogAsync(AuditLogDO auditLog) {
        try {
            auditLog.setCreateTime(LocalDateTime.now());
            auditLogMapper.insert(auditLog);
        } catch (Exception e) {
            log.error("审计日志保存失败", e);
        }
    }

    /**
     * 记录敏感数据访问
     *
     * @param userId 用户ID
     * @param targetType 目标类型
     * @param targetId 目标ID
     * @param actionType 操作类型
     */
    public void recordSensitiveAccess(Long userId,
                                       String targetType,
                                       String targetId,
                                       String actionType) {
        if (!securityProperties.getAudit().getLogSensitiveAccess()) {
            return;
        }

        AuditLogDO auditLog = new AuditLogDO();
        auditLog.setEventType("SENSITIVE_ACCESS");
        auditLog.setUserId(userId);
        auditLog.setTargetType(targetType);
        auditLog.setTargetId(targetId);
        auditLog.setActionType(actionType);
        auditLog.setIsSensitive(true);
        auditLog.setRiskLevel("MEDIUM");

        recordAuditLogAsync(auditLog);
    }

    /**
     * 记录登录事件
     */
    public void recordLogin(Long userId, String username, String ipAddress, boolean success) {
        AuditLogDO auditLog = new AuditLogDO();
        auditLog.setEventType("LOGIN");
        auditLog.setUserId(userId);
        auditLog.setUsername(username);
        auditLog.setIpAddress(ipAddress);
        auditLog.setResult(success ? "SUCCESS" : "FAILURE");
        auditLog.setRiskLevel(success ? "LOW" : "MEDIUM");

        recordAuditLogAsync(auditLog);
    }

    /**
     * 记录数据导出事件
     */
    public void recordDataExport(Long userId,
                                  String module,
                                  Integer affectedRecords) {
        AuditLogDO auditLog = new AuditLogDO();
        auditLog.setEventType("DATA_EXPORT");
        auditLog.setUserId(userId);
        auditLog.setModule(module);
        auditLog.setActionType("EXPORT");
        auditLog.setAffectedRecords(affectedRecords);
        auditLog.setIsSensitive(true);
        auditLog.setRiskLevel(affectedRecords > 100 ? "HIGH" : "MEDIUM");

        recordAuditLogAsync(auditLog);
    }

    /**
     * 记录权限变更事件
     */
    public void recordPermissionChange(Long operatorId,
                                        Long targetUserId,
                                        String permission,
                                        String action) {
        AuditLogDO auditLog = new AuditLogDO();
        auditLog.setEventType("PERMISSION_CHANGE");
        auditLog.setUserId(operatorId);
        auditLog.setTargetType("USER_PERMISSION");
        auditLog.setTargetId(String.valueOf(targetUserId));
        auditLog.setActionType(action);
        auditLog.setDescription(String.format("权限变更:%s", permission));
        auditLog.setIsSensitive(true);
        auditLog.setRiskLevel("HIGH");

        recordAuditLogAsync(auditLog);
    }

    /**
     * 审计注解处理(AOP)
     */
    @Around("@annotation(audit)")
    public Object handleAudit(ProceedingJoinPoint joinPoint, Audit audit) throws Throwable {
        // 1. 获取请求信息
        ServletRequestAttributes attributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes != null ? attributes.getRequest() : null;

        // 2. 构建审计日志
        AuditLogDO auditLog = buildAuditLog(joinPoint, request, audit);

        // 3. 执行方法
        Object result = null;
        try {
            result = joinPoint.proceed();
            auditLog.setResult("SUCCESS");
        } catch (Exception e) {
            auditLog.setResult("FAILURE");
            auditLog.setFailureReason(e.getMessage());
            throw e;
        } finally {
            // 4. 异步保存审计日志
            if (audit.async()) {
                recordAuditLogAsync(auditLog);
            } else {
                // 同步保存
                auditLog.setCreateTime(LocalDateTime.now());
                auditLogMapper.insert(auditLog);
            }
        }

        return result;
    }

    /**
     * 构建审计日志对象
     */
    private AuditLogDO buildAuditLog(ProceedingJoinPoint joinPoint,
                                       HttpServletRequest request,
                                       Audit audit) {
        AuditLogDO auditLog = new AuditLogDO();

        // 方法信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        auditLog.setEventType(audit.eventType());
        auditLog.setModule(audit.module());
        auditLog.setActionType(audit.actionType());
        auditLog.setDescription(audit.description());
        auditLog.setRiskLevel(audit.riskLevel().name());
        auditLog.setIsSensitive(audit.isSensitive());

        // 请求信息
        if (request != null) {
            auditLog.setIpAddress(ServletUtil.getClientIP(request));
            auditLog.setUserAgent(request.getHeader("User-Agent"));
            auditLog.setRequestUri(request.getRequestURI());
            auditLog.setRequestMethod(request.getMethod());

            // 脱敏处理请求参数
            String params = JSONUtil.toJsonStr(request.getParameterMap());
            auditLog.setRequestParams(maskSensitiveParams(params));
        }

        return auditLog;
    }

    /**
     * 脱敏处理请求参数
     */
    private String maskSensitiveParams(String params) {
        if (StrUtil.isBlank(params)) {
            return params;
        }
        // 移除敏感字段
        return params.replaceAll("\"password\":\"[^\"]+\"", "\"password\":\"****\"")
                .replaceAll("\"idCard\":\"[^\"]+\"", "\"idCard\":\"****\"")
                .replaceAll("\"phone\":\"[^\"]+\"", "\"phone\":\"****\"");
    }
}

4.3 审计注解定义

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import java.lang.annotation.*;

/**
 * 审计日志注解
 *
 * 用于标记需要审计的方法
 *
 * @author 点狮信息
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Audit {

    /**
     * 事件类型
     */
    String eventType() default "OPERATION";

    /**
     * 业务模块
     */
    String module() default "";

    /**
     * 操作类型
     */
    String actionType() default "OPERATE";

    /**
     * 操作描述
     */
    String description() default "";

    /**
     * 风险等级
     */
    RiskLevel riskLevel() default RiskLevel.LOW;

    /**
     * 是否敏感操作
     */
    boolean isSensitive() default false;

    /**
     * 是否异步记录
     */
    boolean async() default true;
}

/**
 * 风险等级枚举
 */
enum RiskLevel {
    LOW,
    MEDIUM,
    HIGH,
    CRITICAL
}

4.4 审计日志查询与分析

审计查询服务

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pointlion.cloud.module.hrm.dal.dataobject.security.AuditLogDO;
import com.pointlion.cloud.module.hrm.dal.mysql.security.AuditLogMapper;
import com.pointlion.cloud.module.hrm.service.security.vo.AuditLogQueryVO;
import com.pointlion.cloud.module.hrm.service.security.vo.AuditLogStatisticsVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 审计日志查询服务
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class AuditLogQueryService {

    @Resource
    private AuditLogMapper auditLogMapper;

    /**
     * 分页查询审计日志
     */
    public Page<AuditLogDO> queryAuditLogs(AuditLogQueryVO queryVO) {
        Page<AuditLogDO> page = new Page<>(queryVO.getPageNo(), queryVO.getPageSize());

        LambdaQueryWrapper<AuditLogDO> wrapper = new LambdaQueryWrapper<>();

        // 按事件类型筛选
        if (queryVO.getEventType() != null) {
            wrapper.eq(AuditLogDO::getEventType, queryVO.getEventType());
        }

        // 按用户筛选
        if (queryVO.getUserId() != null) {
            wrapper.eq(AuditLogDO::getUserId, queryVO.getUserId());
        }

        // 按模块筛选
        if (queryVO.getModule() != null) {
            wrapper.eq(AuditLogDO::getModule, queryVO.getModule());
        }

        // 按风险等级筛选
        if (queryVO.getRiskLevel() != null) {
            wrapper.eq(AuditLogDO::getRiskLevel, queryVO.getRiskLevel());
        }

        // 按时间范围筛选
        if (queryVO.getStartTime() != null) {
            wrapper.ge(AuditLogDO::getCreateTime, queryVO.getStartTime());
        }
        if (queryVO.getEndTime() != null) {
            wrapper.le(AuditLogDO::getCreateTime, queryVO.getEndTime());
        }

        // 按敏感操作筛选
        if (queryVO.getIsSensitive() != null) {
            wrapper.eq(AuditLogDO::getIsSensitive, queryVO.getIsSensitive());
        }

        // 按创建时间倒序
        wrapper.orderByDesc(AuditLogDO::getCreateTime);

        return auditLogMapper.selectPage(page, wrapper);
    }

    /**
     * 查询敏感操作日志
     */
    public List<AuditLogDO> querySensitiveLogs(LocalDateTime startTime,
                                                LocalDateTime endTime) {
        LambdaQueryWrapper<AuditLogDO> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(AuditLogDO::getIsSensitive, true)
                .between(AuditLogDO::getCreateTime, startTime, endTime)
                .orderByDesc(AuditLogDO::getCreateTime);

        return auditLogMapper.selectList(wrapper);
    }

    /**
     * 统计审计日志
     */
    public AuditLogStatisticsVO statistics(LocalDate startDate, LocalDate endDate) {
        LocalDateTime startTime = startDate.atStartOfDay();
        LocalDateTime endTime = endDate.atTime(23, 59, 59);

        AuditLogStatisticsVO stats = new AuditLogStatisticsVO();

        // 总操作数
        stats.setTotalCount(auditLogMapper.selectCount(
                new LambdaQueryWrapper<AuditLogDO>()
                        .between(AuditLogDO::getCreateTime, startTime, endTime)
        ));

        // 敏感操作数
        stats.setSensitiveCount(auditLogMapper.selectCount(
                new LambdaQueryWrapper<AuditLogDO>()
                        .eq(AuditLogDO::getIsSensitive, true)
                        .between(AuditLogDO::getCreateTime, startTime, endTime)
        ));

        // 失败操作数
        stats.setFailureCount(auditLogMapper.selectCount(
                new LambdaQueryWrapper<AuditLogDO>()
                        .eq(AuditLogDO::getResult, "FAILURE")
                        .between(AuditLogDO::getCreateTime, startTime, endTime)
        ));

        // 高风险操作数
        stats.setHighRiskCount(auditLogMapper.selectCount(
                new LambdaQueryWrapper<AuditLogDO>()
                        .in(AuditLogDO::getRiskLevel, "HIGH", "CRITICAL")
                        .between(AuditLogDO::getCreateTime, startTime, endTime)
        ));

        // 按事件类型统计
        stats.setEventTypeStats(auditLogMapper.countByEventType(startTime, endTime));

        // 按用户统计
        stats.setUserStats(auditLogMapper.countByUser(startTime, endTime));

        return stats;
    }

    /**
     * 检测异常行为
     */
    public List<AnomalyDetectionVO> detectAnomalies(LocalDateTime startTime,
                                                      LocalDateTime endTime) {
        // 检测以下异常行为:
        // 1. 短时间内大量数据导出
        // 2. 异常时间段的敏感操作
        // 3. 同一IP多个账号登录
        // 4. 大量失败的操作

        return null;
    }
}

五、SQL注入防护与API安全

5.1 SQL注入防护

MyBatis-Plus安全配置

java 复制代码
package com.pointlion.cloud.module.hrm.service.security.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis-Plus安全配置
 *
 * @author 点狮信息
 */
@Configuration
public class MybatisPlusSecurityConfig {

    /**
     * 配置MyBatis-Plus拦截器
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 1. 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        // 2. 防全表更新和删除插件
        // 防止恶意的全表更新/删除操作
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());

        // 3. 乐观锁插件
        // 防止并发修改导致的数据不一致
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

        return interceptor;
    }
}

参数化查询示例

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.pointlion.cloud.module.hrm.dal.dataobject.employee.EmployeeDO;
import com.pointlion.cloud.module.hrm.dal.mysql.employee.EmployeeMapper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * 安全查询示例
 *
 * 演示如何安全地进行数据库查询,防止SQL注入
 *
 * @author 点狮信息
 */
@Service
public class SecureQueryExample {

    @Resource
    private EmployeeMapper employeeMapper;

    /**
     * ✅ 安全的查询方式 - 使用LambdaQueryWrapper
     *
     * MyBatis-Plus会自动进行参数化查询,防止SQL注入
     */
    public List<EmployeeDO> findByNameSafe(String name) {
        return employeeMapper.selectList(
                new LambdaQueryWrapper<EmployeeDO>()
                        .eq(EmployeeDO::getName, name)
        );
    }

    /**
     * ❌ 危险的查询方式 - 字符串拼接
     *
     * 永远不要这样做!
     */
    /*
    public List<EmployeeDO> findByNameUnsafe(String name) {
        return employeeMapper.selectList(
                new QueryWrapper<EmployeeDO>()
                        .apply("name = '" + name + "'") // 危险!SQL注入风险
        );
    }
    */

    /**
     * ✅ 安全的模糊查询
     */
    public List<EmployeeDO> findByPhoneLikeSafe(String phone) {
        return employeeMapper.selectList(
                new LambdaQueryWrapper<EmployeeDO>()
                        .like(EmployeeDO::getPhone, phone)
        );
    }

    /**
     * ✅ 安全的IN查询
     */
    public List<EmployeeDO> findByIdsSafe(List<Long> ids) {
        return employeeMapper.selectList(
                new LambdaQueryWrapper<EmployeeDO>()
                        .in(EmployeeDO::getId, ids)
        );
    }

    /**
     * ✅ 安全的动态条件查询
     */
    public List<EmployeeDO> dynamicQuerySafe(String name, String departmentId, String status) {
        LambdaQueryWrapper<EmployeeDO> wrapper = new LambdaQueryWrapper<>();

        if (name != null && !name.isEmpty()) {
            wrapper.like(EmployeeDO::getName, name);
        }
        if (departmentId != null && !departmentId.isEmpty()) {
            wrapper.eq(EmployeeDO::getDepartmentId, departmentId);
        }
        if (status != null && !status.isEmpty()) {
            wrapper.eq(EmployeeDO::getStatus, status);
        }

        return employeeMapper.selectList(wrapper);
    }
}

5.2 API安全防护

API访问限流

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;

/**
 * API访问限流服务
 *
 * 防止API滥用和恶意爬取
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class RateLimitService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 检查是否超过限流阈值
     *
     * @param key 限流Key
     * @param maxRequests 最大请求数
     * @param duration 时间窗口
     * @return true-未超限,false-已超限
     */
    public boolean checkRateLimit(String key, int maxRequests, Duration duration) {
        String countKey = "ratelimit:" + key;

        // 1. 增加计数
        Long count = stringRedisTemplate.opsForValue().increment(countKey);

        if (count == null) {
            count = 0L;
        }

        // 2. 首次访问,设置过期时间
        if (count == 1) {
            stringRedisTemplate.expire(countKey, duration);
        }

        // 3. 检查是否超限
        if (count > maxRequests) {
            log.warn("API访问超限: key={}, count={}", key, count);
            return false;
        }

        return true;
    }

    /**
     * 检查用户API访问限流
     *
     * @param userId 用户ID
     * @param apiPath API路径
     * @return true-未超限
     */
    public boolean checkUserApiRateLimit(Long userId, String apiPath) {
        String key = "user:" + userId + ":api:" + apiPath;
        // 每分钟最多60次请求
        return checkRateLimit(key, 60, Duration.ofMinutes(1));
    }

    /**
     * 检查IP访问限流
     *
     * @param ipAddress IP地址
     * @param apiPath API路径
     * @return true-未超限
     */
    public boolean checkIpRateLimit(String ipAddress, String apiPath) {
        String key = "ip:" + ipAddress + ":api:" + apiPath;
        // 每分钟最多100次请求
        return checkRateLimit(key, 100, Duration.ofMinutes(1));
    }

    /**
     * 检查数据导出限流
     *
     * @param userId 用户ID
     * @return true-未超限
     */
    public boolean checkDataExportLimit(Long userId) {
        String key = "export:" + userId;
        // 每小时最多5次导出
        return checkRateLimit(key, 5, Duration.ofHours(1));
    }

    /**
     * 获取剩余访问次数
     */
    public long getRemainingRequests(String key, int maxRequests) {
        String countKey = "ratelimit:" + key;
        String countStr = stringRedisTemplate.opsForValue().get(countKey);
        long count = countStr != null ? Long.parseLong(countStr) : 0;
        return Math.max(0, maxRequests - count);
    }
}

API签名验证

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import cn.hutool.crypto.digest.DigestUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.Map;
import java.util.TreeMap;

/**
 * API签名验证服务
 *
 * 防止API请求被篡改和重放攻击
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class ApiSignatureService {

    /**
     * 验证API签名
     *
     * @param apiKey API Key
     * @param timestamp 时间戳
     * @param nonce 随机数
     * @param signature 签名
     * @param params 请求参数
     * @return true-签名有效
     */
    public boolean verifySignature(String apiKey,
                                     String timestamp,
                                     String nonce,
                                     String signature,
                                     Map<String, String> params) {
        try {
            // 1. 检查时间戳(防重放攻击)
            long requestTime = Long.parseLong(timestamp);
            long currentTime = Instant.now().getEpochSecond();
            if (Math.abs(currentTime - requestTime) > 300) {
                log.warn("API签名验证失败:时间戳过期,requestTime={}, currentTime={}",
                        requestTime, currentTime);
                return false;
            }

            // 2. 检查nonce(防重放攻击)
            if (isNonceUsed(nonce)) {
                log.warn("API签名验证失败:nonce重复使用,nonce={}", nonce);
                return false;
            }

            // 3. 计算预期签名
            String expectedSignature = calculateSignature(apiKey, timestamp, nonce, params);

            // 4. 比对签名
            boolean isValid = expectedSignature.equals(signature);

            if (isValid) {
                // 标记nonce已使用
                markNonceUsed(nonce);
            } else {
                log.warn("API签名验证失败:签名不匹配,expected={}, actual={}",
                        expectedSignature, signature);
            }

            return isValid;

        } catch (Exception e) {
            log.error("API签名验证异常", e);
            return false;
        }
    }

    /**
     * 计算签名
     *
     * 签名算法:SHA256(apiKey + timestamp + nonce + sorted(params))
     */
    private String calculateSignature(String apiKey,
                                        String timestamp,
                                        String nonce,
                                        Map<String, String> params) {
        // 1. 参数排序
        TreeMap<String, String> sortedParams = new TreeMap<>(params);

        // 2. 构建签名字符串
        StringBuilder sb = new StringBuilder();
        sb.append(apiKey);
        sb.append(timestamp);
        sb.append(nonce);

        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            sb.append(entry.getKey());
            sb.append("=");
            sb.append(entry.getValue());
            sb.append("&");
        }

        // 3. SHA256哈希
        return DigestUtil.sha256Hex(sb.toString());
    }

    /**
     * 检查nonce是否已使用
     */
    private boolean isNonceUsed(String nonce) {
        // 检查Redis中是否存在该nonce
        // 实际实现应使用Redis,这里简化
        return false;
    }

    /**
     * 标记nonce已使用
     */
    private void markNonceUsed(String nonce) {
        // 将nonce存入Redis,设置5分钟过期
        // 实际实现应使用Redis
    }

    /**
     * 生成API签名
     */
    public String generateSignature(String apiKey,
                                     String timestamp,
                                     String nonce,
                                     Map<String, String> params) {
        return calculateSignature(apiKey, timestamp, nonce, params);
    }
}

六、数据备份与恢复

6.1 数据备份策略

备份服务实现

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 数据备份服务
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class DataBackupService {

    @Value("${hrm.backup.path:/data/backup}")
    private String backupPath;

    /**
     * 每日凌晨2点执行备份
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void dailyBackup() {
        log.info("开始执行每日数据备份...");
        try {
            String backupFile = performBackup("daily");
            log.info("每日数据备份完成,备份文件:{}", backupFile);
        } catch (Exception e) {
            log.error("每日数据备份失败", e);
        }
    }

    /**
     * 执行备份
     *
     * @param backupType 备份类型(daily, weekly, monthly)
     * @return 备份文件路径
     */
    public String performBackup(String backupType) throws Exception {
        // 1. 生成备份文件名
        String timestamp = LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
        String backupFileName = String.format("hrm_backup_%s_%s.zip", backupType, timestamp);
        String backupFilePath = backupPath + "/" + backupFileName;

        // 2. 创建备份目录
        File backupDir = new File(backupPath);
        if (!backupDir.exists()) {
            backupDir.mkdirs();
        }

        // 3. 执行数据库备份
        String dbBackupFile = backupDatabase();

        // 4. 打包备份文件
        try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(backupFilePath))) {
            // 添加数据库备份
            addToZip(zipOut, dbBackupFile, "database.sql");

            // 添加上传文件备份
            String uploadBackupPath = backupUploadFiles();
            if (uploadBackupPath != null) {
                addToZip(zipOut, uploadBackupPath, "uploads/");
            }

            // 添加备份元数据
            String metadata = generateBackupMetadata(backupType);
            addMetadataToZip(zipOut, metadata, "metadata.json");
        }

        // 5. 上传到远程存储(可选)
        uploadToRemoteStorage(backupFilePath);

        // 6. 清理本地临时文件
        cleanupTempFiles(dbBackupFile);

        return backupFilePath;
    }

    /**
     * 备份数据库
     */
    private String backupDatabase() throws Exception {
        String timestamp = LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
        String backupFile = backupPath + "/db_backup_" + timestamp + ".sql";

        // 使用mysqldump备份数据库
        ProcessBuilder pb = new ProcessBuilder(
                "mysqldump",
                "-h", "localhost",
                "-u", "root",
                "-p" + System.getenv("MYSQL_PASSWORD"),
                "--single-transaction",
                "--routines",
                "--triggers",
                "--databases", "hrm"
        );

        pb.redirectErrorStream(true);
        Process process = pb.start();

        try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()));
             FileWriter writer = new FileWriter(backupFile)) {

            String line;
            while ((line = reader.readLine()) != null) {
                writer.write(line);
                writer.write("\n");
            }
        }

        int exitCode = process.waitFor();
        if (exitCode != 0) {
            throw new RuntimeException("数据库备份失败,退出码:" + exitCode);
        }

        return backupFile;
    }

    /**
     * 备份上传文件
     */
    private String backupUploadFiles() throws Exception {
        // 备份用户上传的文件(证件照、附件等)
        // 实际实现应使用rsync或类似工具
        return null;
    }

    /**
     * 生成备份元数据
     */
    private String generateBackupMetadata(String backupType) {
        return String.format("""
                {
                    "backupType": "%s",
                    "timestamp": "%s",
                    "version": "%s",
                    "database": "hrm",
                    "tables": ["hrm_employee", "hrm_salary", "hrm_attendance"]
                }
                """,
                backupType,
                LocalDateTime.now(),
                "1.0.0"
        );
    }

    /**
     * 添加文件到ZIP
     */
    private void addToZip(ZipOutputStream zipOut, String filePath, String entryName) throws IOException {
        File file = new File(filePath);
        if (file.exists()) {
            ZipEntry zipEntry = new ZipEntry(entryName);
            zipOut.putNextEntry(zipEntry);

            try (FileInputStream fis = new FileInputStream(file)) {
                byte[] buffer = new byte[1024];
                int length;
                while ((length = fis.read(buffer)) > 0) {
                    zipOut.write(buffer, 0, length);
                }
            }

            zipOut.closeEntry();
        }
    }

    /**
     * 添加元数据到ZIP
     */
    private void addMetadataToZip(ZipOutputStream zipOut, String metadata, String entryName) throws IOException {
        ZipEntry zipEntry = new ZipEntry(entryName);
        zipOut.putNextEntry(zipEntry);
        zipOut.write(metadata.getBytes());
        zipOut.closeEntry();
    }

    /**
     * 上传到远程存储
     */
    private void uploadToRemoteStorage(String localFilePath) {
        // 上传到阿里云OSS、AWS S3等
        // 实现远程备份,确保数据安全
    }

    /**
     * 清理临时文件
     */
    private void cleanupTempFiles(String... tempFiles) {
        for (String tempFile : tempFiles) {
            if (tempFile != null) {
                new File(tempFile).delete();
            }
        }
    }

    /**
     * 每周备份(周日执行)
     */
    @Scheduled(cron = "0 0 3 ? * SUN")
    public void weeklyBackup() {
        log.info("开始执行每周数据备份...");
        try {
            String backupFile = performBackup("weekly");
            log.info("每周数据备份完成,备份文件:{}", backupFile);
        } catch (Exception e) {
            log.error("每周数据备份失败", e);
        }
    }
}

6.2 数据恢复服务

恢复服务实现

java 复制代码
package com.pointlion.cloud.module.hrm.service.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * 数据恢复服务
 *
 * @author 点狮信息
 */
@Slf4j
@Service
public class DataRestoreService {

    @Value("${hrm.backup.path:/data/backup}")
    private String backupPath;

    /**
     * 从备份文件恢复数据
     *
     * @param backupFile 备份文件路径
     */
    public void restoreFromBackup(String backupFile) throws Exception {
        log.warn("开始从备份文件恢复数据:{}", backupFile);

        // 1. 验证备份文件
        if (!validateBackupFile(backupFile)) {
            throw new IllegalArgumentException("备份文件无效或已损坏");
        }

        // 2. 解压备份文件
        String tempDir = extractBackupFile(backupFile);

        // 3. 恢复数据库
        String sqlFile = tempDir + "/database.sql";
        restoreDatabase(sqlFile);

        // 4. 恢复上传文件
        restoreUploadFiles(tempDir);

        // 5. 清理临时文件
        cleanupDirectory(new File(tempDir));

        log.warn("数据恢复完成");
    }

    /**
     * 验证备份文件
     */
    private boolean validateBackupFile(String backupFile) {
        File file = new File(backupFile);
        if (!file.exists() || !file.isFile()) {
            return false;
        }

        // 检查ZIP文件是否有效
        try (ZipInputStream zis = new ZipInputStream(new FileInputStream(file))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                if ("metadata.json".equals(entry.getName())) {
                    return true;
                }
            }
        } catch (IOException e) {
            log.error("验证备份文件失败", e);
            return false;
        }

        return false;
    }

    /**
     * 解压备份文件
     */
    private String extractBackupFile(String backupFile) throws IOException {
        String tempDir = backupPath + "/temp_" + System.currentTimeMillis();
        new File(tempDir).mkdirs();

        try (ZipInputStream zis = new ZipInputStream(new FileInputStream(backupFile))) {
            ZipEntry entry;
            byte[] buffer = new byte[1024];
            while ((entry = zis.getNextEntry()) != null) {
                File newFile = new File(tempDir, entry.getName());
                if (entry.isDirectory()) {
                    newFile.mkdirs();
                } else {
                    new File(newFile.getParent()).mkdirs();
                    try (FileOutputStream fos = new FileOutputStream(newFile)) {
                        int len;
                        while ((len = zis.read(buffer)) > 0) {
                            fos.write(buffer, 0, len);
                        }
                    }
                }
                zis.closeEntry();
            }
        }

        return tempDir;
    }

    /**
     * 恢复数据库
     */
    private void restoreDatabase(String sqlFile) throws Exception {
        log.warn("开始恢复数据库...");

        ProcessBuilder pb = new ProcessBuilder(
                "mysql",
                "-h", "localhost",
                "-u", "root",
                "-p" + System.getenv("MYSQL_PASSWORD"),
                "hrm"
        );

        Process process = pb.start();

        try (BufferedReader reader = new BufferedReader(new FileReader(sqlFile));
             OutputStream writer = process.getOutputStream()) {

            String line;
            while ((line = reader.readLine()) != null) {
                writer.write(line.getBytes());
                writer.write("\n".getBytes());
            }
        }

        int exitCode = process.waitFor();
        if (exitCode != 0) {
            throw new RuntimeException("数据库恢复失败,退出码:" + exitCode);
        }

        log.warn("数据库恢复完成");
    }

    /**
     * 恢复上传文件
     */
    private void restoreUploadFiles(String tempDir) {
        // 恢复用户上传的文件
        // 实现文件恢复逻辑
    }

    /**
     * 清理目录
     */
    private void cleanupDirectory(File directory) {
        File[] files = directory.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    cleanupDirectory(file);
                } else {
                    file.delete();
                }
            }
        }
        directory.delete();
    }

    /**
     * 列出可用的备份文件
     */
    public File[] listAvailableBackups() {
        File backupDir = new File(backupPath);
        if (backupDir.exists() && backupDir.isDirectory()) {
            return backupDir.listFiles((dir, name) -> name.endsWith(".zip"));
        }
        return new File[0];
    }
}

七、前端安全实现

7.1 Vue3数据脱敏组件

脱敏显示组件

vue 复制代码
<template>
  <span v-if="masked">{{ maskedValue }}</span>
  <span v-else>{{ value }}</span>
  <el-button
    v-if="canToggle && !alwaysMask"
    link
    type="primary"
    @click="toggleMask"
  >
    {{ masked ? '显示' : '隐藏' }}
  </el-button>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'

interface Props {
  value?: string
  type?: 'idCard' | 'phone' | 'bankCard' | 'email' | 'salary'
  canToggle?: boolean
  alwaysMask?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  value: '',
  type: 'idCard',
  canToggle: false,
  alwaysMask: false
})

const masked = ref(props.alwaysMask)

const maskedValue = computed(() => {
  if (!props.value) return ''

  switch (props.type) {
    case 'idCard':
      // 身份证号:保留前6位和后4位
      return props.value.replace(/^(.{6}).*(.{4})$/, '$1********$2')

    case 'phone':
      // 手机号:保留前3位和后4位
      return props.value.replace(/^(.{3}).*(.{4})$/, '$1****$2')

    case 'bankCard':
      // 银行卡:保留前4位和后4位
      return props.value.replace(/^(.{4}).*(.{4})$/, '$1******$2')

    case 'email':
      // 邮箱:保留第一个字符和@后的域名
      const parts = props.value.split('@')
      if (parts.length === 2) {
        return `${parts[0].charAt(0)}***@${parts[1]}`
      }
      return props.value

    case 'salary':
      // 薪资:显示范围
      const salary = parseInt(props.value)
      const range = Math.floor(salary / 5000) * 5
      return `${range}k-${range + 5}k`

    default:
      return '****'
  }
})

const toggleMask = () => {
  masked.value = !masked.value
}
</script>

7.2 安全的HTTP请求封装

请求拦截器

typescript 复制代码
// src/utils/request/security.ts

import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/modules/user'

const securityRequest = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 30000
})

// 请求拦截器
securityRequest.interceptors.request.use(
  (config) => {
    const userStore = useUserStore()

    // 1. 添加认证token
    if (userStore.token) {
      config.headers['Authorization'] = `Bearer ${userStore.token}`
    }

    // 2. 添加请求签名(防篡改)
    const timestamp = Date.now()
    const nonce = Math.random().toString(36).substring(7)
    config.headers['X-Timestamp'] = timestamp.toString()
    config.headers['X-Nonce'] = nonce

    // 3. 计算签名
    const signature = calculateSignature(
      userStore.token || '',
      timestamp.toString(),
      nonce,
      config.data || {}
    )
    config.headers['X-Signature'] = signature

    // 4. 添加CSRF token
    config.headers['X-CSRF-Token'] = getCsrfToken()

    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
securityRequest.interceptors.response.use(
  (response) => {
    // 1. 检查响应签名
    const responseSignature = response.headers['x-signature']
    if (responseSignature) {
      if (!verifyResponseSignature(response.data, responseSignature)) {
        ElMessage.error('响应数据可能被篡改')
        return Promise.reject(new Error('响应签名验证失败'))
      }
    }

    return response.data
  },
  (error) => {
    // 2. 处理敏感错误信息
    if (error.response?.status === 403) {
      ElMessage.error('无权限访问该资源')
    } else if (error.response?.status === 401) {
      ElMessage.error('登录已过期,请重新登录')
      const userStore = useUserStore()
      userStore.logout()
    } else {
      ElMessage.error(error.response?.data?.msg || '请求失败')
    }

    return Promise.reject(error)
  }
)

/**
 * 计算请求签名
 */
function calculateSignature(
  apiKey: string,
  timestamp: string,
  nonce: string,
  data: any
): string {
  const crypto = require('crypto')

  // 1. 参数排序
  const sortedParams = Object.keys(data)
    .sort()
    .map((key) => `${key}=${data[key]}`)
    .join('&')

  // 2. 构建签名字符串
  const signString = `${apiKey}${timestamp}${nonce}${sortedParams}`

  // 3. SHA256哈希
  return crypto.createHash('sha256').update(signString).digest('hex')
}

/**
 * 验证响应签名
 */
function verifyResponseSignature(data: any, signature: string): boolean {
  const crypto = require('crypto')
  const expectedSignature = crypto
    .createHash('sha256')
    .update(JSON.stringify(data))
    .digest('hex')

  return expectedSignature === signature
}

/**
 * 获取CSRF token
 */
function getCsrfToken(): string {
  const metaTag = document.querySelector('meta[name="csrf-token"]')
  return metaTag?.getAttribute('content') || ''
}

export default securityRequest

八、安全最佳实践与合规

8.1 安全开发规范

编码规范检查清单

java 复制代码
package com.pointlion.cloud.module.hrm.service.security.checklist;

/**
 * 安全开发规范检查清单
 *
 * @author 点狮信息
 */
public class SecurityChecklist {

    /**
     * ✅ 数据安全检查清单
     *
     * 1. 敏感数据是否加密存储?
     * 2. 加密算法是否使用AES-256-GCM?
     * 3. 密钥是否定期轮换?
     * 4. 传输是否使用HTTPS/TLS?
     * 5. 数据库连接是否使用SSL?
     */

    /**
     * ✅ 访问控制检查清单
     *
     * 1. 是否实现了最小权限原则?
     * 2. 敏感操作是否需要二次验证?
     * 3. 权限变更是否审计?
     * 4. 会话是否合理超时?
     * 5. 是否实现了防越权访问?
     */

    /**
     * ✅ 输入验证检查清单
     *
     * 1. 所有用户输入是否验证?
     * 2. SQL查询是否参数化?
     * 3. XSS过滤是否到位?
     * 4. 文件上传是否校验类型?
     * 5. API参数是否限制长度?
     */

    /**
     * ✅ 日志与审计检查清单
     *
     * 1. 敏感操作是否记录日志?
     * 2. 日志是否脱敏处理?
     * 3. 异常行为是否检测?
     * 4. 日志是否防篡改?
     * 5. 日志是否定期归档?
     */

    /**
     * ✅ 依赖安全检查清单
     *
     * 1. 第三方库是否定期更新?
     * 2. 是否扫描已知漏洞?
     * 3. 是否禁用不必要的功能?
     * 4. Maven依赖是否审查?
     * 5. npm包是否验证完整性?
     */
}

8.2 合规性检查

个人信息保护影响评估(PIA)

java 复制代码
package com.pointlion.cloud.module.hrm.service.security.compliance;

import lombok.Data;

import java.util.List;

/**
 * 个人信息保护影响评估
 *
 * 用于评估HRM系统对个人信息的处理是否符合法规要求
 *
 * @author 点狮信息
 */
@Data
public class PrivacyImpactAssessment {

    /**
     * 评估ID
     */
    private String assessmentId;

    /**
     * 评估日期
     */
    private String assessmentDate;

    /**
     * 评估人员
     */
    private String assessor;

    /**
     * 评估结果
     */
    private AssessmentResult result;

    /**
     * 风险项列表
     */
    private List<RiskItem> riskItems;

    /**
     * 整改措施
     */
    private List<RemediationAction> remediationActions;

    /**
     * 评估结论
     */
    private String conclusion;

    @Data
    public static class AssessmentResult {
        /**
         * 风险等级:LOW, MEDIUM, HIGH, CRITICAL
         */
        private String riskLevel;

        /**
         * 合规性评分(0-100)
         */
        private Integer complianceScore;

        /**
         * 是否符合要求
         */
        private Boolean isCompliant;
    }

    @Data
    public static class RiskItem {
        /**
         * 风险描述
         */
        private String description;

        /**
         * 风险等级
         */
        private String level;

        /**
         * 影响范围
         */
        private String scope;

        /**
         * 可能性(HIGH, MEDIUM, LOW)
         */
        private String likelihood;
    }

    @Data
    public static class RemediationAction {
        /**
         * 措施描述
         */
        private String description;

        /**
         * 优先级
         */
        private String priority;

        /**
         * 负责人
         */
        private String owner;

        /**
         * 截止日期
         */
        private String deadline;

        /**
         * 状态
         */
        private String status;
    }

    /**
     * 执行评估
     */
    public void performAssessment() {
        // 1. 数据收集评估
        assessDataCollection();

        // 2. 数据处理评估
        assessDataProcessing();

        // 3. 数据存储评估
        assessDataStorage();

        // 4. 数据共享评估
        assessDataSharing();

        // 5. 数据删除评估
        assessDataDeletion();

        // 6. 权限控制评估
        assessAccessControl();

        // 7. 安全措施评估
        assessSecurityMeasures();

        // 8. 生成结论
        generateConclusion();
    }

    private void assessDataCollection() {
        // 评估数据收集是否符合最小化原则
    }

    private void assessDataProcessing() {
        // 评估数据处理是否获得用户同意
    }

    private void assessDataStorage() {
        // 评估数据存储是否加密
    }

    private void assessDataSharing() {
        // 评估数据共享是否有授权
    }

    private void assessDataDeletion() {
        // 评估数据删除是否彻底
    }

    private void assessAccessControl() {
        // 评估访问控制是否严格
    }

    private void assessSecurityMeasures() {
        // 评估安全措施是否充分
    }

    private void generateConclusion() {
        // 生成评估结论
    }
}

九、总结

企业HRM系统的安全建设是一个系统工程,需要从技术、流程、人员多个维度综合考虑。本文从实际代码实现出发,展示了如何构建一个完整的HRM安全体系:

核心安全措施

  1. 数据加密:AES-256-GCM加密敏感字段,HTTPS/TLS保护传输,密钥定期轮换
  2. 访问控制:基于RBAC的数据权限模型,薪资数据特殊保护,最小权限原则
  3. 审计追踪:全面记录敏感操作,支持异常行为检测,满足合规审计需求
  4. API安全:请求签名验证,访问限流,SQL注入防护
  5. 数据备份:定期自动备份,支持快速恢复,远程存储保障

合规性保障

  • 符合《个人信息保护法》对敏感个人信息的要求
  • 实现数据分类分级保护
  • 提供完整的审计追踪能力
  • 支持隐私影响评估

持续改进方向

  1. 引入零信任架构,进一步加强身份认证
  2. 使用AI技术检测异常行为模式
  3. 加强数据防泄漏(DLP)措施
  4. 提升安全运营自动化水平
相关推荐
utmhikari9 分钟前
【日常随笔】深入回答纯Vibe Coding写后端项目的几个问题
后端·ai编程·vibecoding
尚早立志19 分钟前
Spring Boot 源码研读之ConfigurableEnvironment 环境准备
java·spring boot·后端
TanYYF33 分钟前
spring ai入门教程一
java·人工智能·spring
红糖奶茶1 小时前
【实测有效】 如何关闭Windows自动更新?【图文详解】win10/win11关闭自动更新
其他·安全
布朗克1681 小时前
Go 入门到精通-08-复合类型之数组与切片
开发语言·后端·golang·数组与切片
fliter1 小时前
从手写 HTTP/1.1 到拆开 HTTP/2
后端
CaffeinePro1 小时前
FastAPI自动接口文档定制与美化、权限管控
后端·fastapi
A-刘晨阳1 小时前
关键基础设施安全底座:自主可控时序大模型TimechoAI的国产化实践与深度时序分析能力
大数据·数据库·安全·时序数据库
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第151题】【06_Spring篇】第11题:说一下 Spring Bean 的生命周期?
java·开发语言·后端·spring·面试
江畔柳前堤1 小时前
第17章:Docker 大厂面试题精选(腾讯/阿里/字节/美团)
运维·网络·spring cloud·docker·容器·eureka