点狮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. 提升安全运营自动化水平
相关推荐
西安邮电大学1 小时前
有关数组的经典算法题
java·后端·其他·算法·面试
mikasa6671 小时前
关于Spring MVC 基于 AOP 实现的全局控制器统一处理方案@ControllerAdvice
java·spring·mvc
摇滚侠1 小时前
SpringMVC 入门到实战 SpringMVC 的执行流程 96
java·后端·spring·maven·intellij-idea
布朗克1682 小时前
38 Spring Boot入门——自动配置、核心注解与Starter机制
java·spring boot·后端
程序员老申2 小时前
外呼突然全挂了,追查 24 分钟后我发现了 etcd 最阴的一颗雷
后端·程序员
何以解忧,唯有..2 小时前
Go语言变量的声明方式详解
开发语言·后端·golang
长栎2 小时前
MyBatis 缓存为啥总是失效?装饰器模式套娃的代价
后端
bright_ye2 小时前
setjmp & longjmp 深度详解 + 代码示例
后端
To_OC2 小时前
我一直以为 Ajax 是个黑盒,直到我写了这 50 行代码
前端·后端·全栈