Java 安全与合规:安全编码、数据脱敏与供应链防护

覆盖 OWASP Top 10 安全编码规范、输入校验与注入防护、数据脱敏实现、GDPR/等保合规映射,以及 SBOM 与依赖漏洞扫描等供应链安全实践

安全不是某个安全团队的专属工作,而是每一个后端工程师的编码习惯。在数据泄露事故频发、GDPR 与等保 2.0 监管趋严的今天,「能跑」的代码与「安全合规」的代码之间,隔着一整套编码规范、注入防护、数据脱敏与供应链管理的工程实践。本文从编码规范到合规落地,给出 Java 应用可执行的安全基线。

前置基础可先阅读 Spring Security Web 安全纵深防御 与 Java 安全认证与授权。

1. 安全编码规范

1.1 OWASP Top 10 与 CWE Top 25

OWASP Top 10(2021)Java 侧典型场景防御要点
A01 失效的访问控制越权访问他人数据服务端鉴权 + 对象级授权
A02 加密失败敏感数据明文存储/传输强制 TLS + 加密存储
A03 注入SQL/命令/XXE参数化 + 白名单校验
A04 不安全设计信任链缺失最小权限 + 默认拒绝
A05 安全配置错误错误信息泄露堆栈隐藏栈、收紧默认配置
A06 脆弱过时组件依赖存在已知 CVE依赖扫描 + 升级策略
A07 身份认证失败弱口令、会话固定强密码策略 + 会话管理
A08 软件与数据完整性反序列化攻击校验签名、安全反序列化
A09 日志与监控缺陷无安全日志审计日志 + 告警
A10 SSRF服务端请求伪造URL 白名单 + 内网隔离

1.2 安全编码三原则

1. 最小权限原则:每个组件只拥有完成任务所需的最小权限
   ├─ 数据库账号只授权必要的库表
   ├─ 服务账号不共享超管凭证
   └─ 云资源按角色授权(IAM)

2. 纵深防御:单点防护会被绕过,多层防线
   ├─ 输入校验 → 参数化 → 权限校验 → 审计
   └─ 任何一层失守,还有下一层兜底

3. 默认拒绝:没有被明确允许的,就是被禁止的
   ├─ 白名单路由、白名单字段
   └─ 异常默认打回,而非放行

1.3 安全函数与工具

// OWASP Java Encoder:正确的 HTML/JS/URL 编码
import org.owasp.encoder.Encode;
String safe = Encode.forHtml(userInput);         // HTML 上下文
String safeJs = Encode.forJavaScript(userInput); // JS 上下文
String safeUrl = Encode.forUriComponent(userInput);

// OWASP HTML Sanitizer:富文本消毒(保留安全标签,剔除脚本)
PolicyFactory policy = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
String cleanHtml = policy.sanitize(userHtml);
工具用途
OWASP Java Encoder上下文感知编码
OWASP HTML Sanitizer富文本 XSS 消毒
find-sec-bugs / SpotBugs Security静态安全扫描(检测硬编码密码、不安全随机、SQL 拼接)
OWASP Dependency-Check依赖 CVE 扫描
# find-sec-bugs 扫描示例
mvn com.github.spotbugs:spotbugs-maven-plugin:check
# 常见检出:CWE-798 硬编码密钥、CWE-89 SQL 注入、CWE-330 不安全随机

2. 输入校验与防注入

2.1 输入校验:白名单优先

// 使用 Bean Validation(见专项文章)做声明式校验
@PostMapping("/users")
public User createUser(@Valid @RequestBody UserRequest req) {
    return userService.create(req);
}

// 自定义正则白名单校验
public class PhoneValidator implements ConstraintValidator<ValidPhone, String> {
    private static final Pattern P = Pattern.compile("^1[3-9]\\d{9}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext ctx) {
        if (value == null) return true;          // null 交给 @NotNull 处理
        return P.matcher(value).matches();       // 白名单:只允许手机号格式
    }
}

校验原则:

  • 长度上限 + 格式白名单 + 枚举白名单三件套;
  • 前端校验只是体验,后端校验才是安全边界;
  • 拒绝非法输入用「白名单正则」,不要用黑名单过滤(容易被绕过)。

2.2 SQL 注入防护

// ✅ JDBC:参数化查询(永远不要字符串拼接 SQL)
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM orders WHERE user_id = ? AND status = ?");
ps.setString(1, userId);
ps.setString(2, status);

// ✅ MyBatis:使用 #{} 而非 ${}
// SELECT * FROM orders WHERE user_id = #{userId}   ← 参数化
// ORDER BY ${sortField}                            ← 仅当值来自白名单

// ✅ JPA:JPQL 参数绑定
TypedQuery<Order> q = em.createQuery(
    "SELECT o FROM Order o WHERE o.userId = :uid", Order.class);
q.setParameter("uid", userId);

${} 使用的底线:当必须动态拼接表名/排序字段时,值必须来自服务端白名单(枚举/映射),绝不能来自用户输入。

2.3 NoSQL 注入与命令注入

// MongoDB:使用 Criteria 构建查询,避免 JS 表达式注入
Query query = new Query(Criteria.where("username").is(username));  // ✅ 参数化

// 危险写法:使用 $where / eval / 正则拼接用户输入构造查询对象
// 防御:所有查询参数走 Criteria 值绑定,正则字段做转义

// 命令注入:永远使用 ProcessBuilder 参数列表,而非拼接 shell
ProcessBuilder pb = new ProcessBuilder("git", "checkout", branch);  // ✅ 参数数组
// ❌ 禁止:Runtime.getRuntime().exec("git checkout " + branch)   // shell 注入

2.4 XXE 与反序列化

// XML 解析必须禁用外部实体(XXE)
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

// 反序列化:不信任来源的 ObjectInputStream 是高危操作
// 防御方案:
//  1. 优先用 JSON 序列化替代 Java 原生序列化
//  2. 必须用时做类白名单校验(ObjectInputFilter)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.example.dto.*;java.lang.*;java.util.*;!*");   // 只允许白名单类
ois.setObjectInputFilter(filter);

2.5 安全的文件上传

// 上传校验:扩展名 + Content-Type + 魔数 + 大小四重校验
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    // 1. 大小限制
    if (file.getSize() > 5 * 1024 * 1024) throw new IllegalArgumentException("too large");
    // 2. 扩展名白名单
    String ext = StringUtils.getFilenameExtension(file.getOriginalFilename());
    if (!Set.of("jpg", "png", "pdf").contains(ext.toLowerCase())) throw new IllegalArgumentException("bad type");
    // 3. 魔数校验(真实文件类型,防伪装)
    byte[] head = new byte[4];
    try (InputStream in = file.getInputStream()) {
        in.read(head);
        if (!(head[0] == (byte)0xFF && head[1] == (byte)0xD8)) throw new IllegalArgumentException("not jpg");
    }
    // 4. 存储:重命名 + 随机文件名 + 单独存储域/对象存储,禁止可执行路径
    String objectKey = UUID.randomUUID() + "." + ext;
    ossClient.putObject(bucket, objectKey, file.getInputStream());
    return objectKey;
}

3. 数据脱敏

3.1 静态脱敏 vs 动态脱敏

维度静态脱敏动态脱敏
场景测试/开发环境、数据导出生产接口返回
时机数据写入目标库前一次性处理每次查询返回时实时处理
实现ETL 脚本、脱敏平台序列化层注解 / 网关改造
效果脱敏后不可逆按权限动态(运维看全量,前端看脱敏)

3.2 字段级脱敏注解实现

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Mask {
    MaskType type() default MaskType.CUSTOM;   // 手机号/身份证/邮箱/银行卡...
}

public enum MaskType {
    PHONE, ID_CARD, EMAIL, BANK_CARD, NAME, ADDRESS
}

// 序列化层脱敏:Jackson 自定义序列化器
public class MaskSerializer extends JsonSerializer<Object> implements ContextualSerializer {
    private MaskType type;

    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider p) throws IOException {
        gen.writeString(mask(type, String.valueOf(value)));
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
        Mask m = property.getAnnotation(Mask.class);
        if (m != null) {
            MaskSerializer s = new MaskSerializer();
            s.type = m.type();
            return s;
        }
        return this;
    }

    public static String mask(MaskType t, String raw) {
        if (raw == null || raw.isEmpty()) return raw;
        switch (t) {
            case PHONE:     return raw.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
            case ID_CARD:   return raw.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2");
            case EMAIL:     return raw.replaceAll("(^\\w{2})[^@]*(@.*)", "$1***$2");
            case BANK_CARD: return raw.replaceAll("(\\d{4})\\d{8}(\\d{4})", "$1********$2");
            case NAME:      return raw.length() > 1 ? raw.charAt(0) + "*".repeat(raw.length() - 1) : "*";
            default:        return "***";
        }
    }
}

// 使用:@Mask 注解字段,序列化时自动脱敏
public class UserVO {
    @Mask(type = MaskType.PHONE)
    private String phone;
    @Mask(type = MaskType.ID_CARD)
    private String idCard;
    // 明文字段内部可用 @JsonIgnore 或不暴露
}

3.3 日志脱敏

日志是泄密重灾区(手机号、身份证、Token 常被整段打印)。方案:

// 方案一:Logback 自定义 Layout,按正则脱敏后写文件
public class MaskingLayout extends PatternLayout {
    private static final Pattern SENSITIVE = Pattern.compile(
        "(phone|mobile|idCard|password|token)=\\s*\"?([^\\s,&]+)\"?");
    @Override
    public String doLayout(ILoggingEvent event) {
        return SENSITIVE.matcher(super.doLayout(event))
            .replaceAll("$1=***");
    }
}
<!-- logback.xml -->
<encoder class="com.example.logging.MaskingLayout">
    <pattern>%d{HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>

3.4 脱敏流程落地

数据生命周期中的脱敏触点:
  入库(静态脱敏/加密)→ 查询(动态脱敏)→ 日志(脱敏)→ 导出(脱敏)
  → 异常堆栈(不打印敏感入参)→ 审计(记录事件,不记录明文)

脱敏策略管理:
  ├─ 脱敏规则按数据分类(个人/金融/位置)统一配置
  ├─ 白名单角色(客服、风控)在审批后可看明文
  └─ 全量脱敏抽样验证:定期核对脱敏字段无明文泄漏

4. 合规:GDPR 与等保

4.1 GDPR 关键要求与 Java 落地

GDPR 要求Java 落地
数据主体访问权提供接口返回该用户全量数据(需鉴权 + 审计)
数据删除权(被遗忘权)删除/匿名化接口,硬删或逻辑删
数据可携带权结构化导出接口(JSON/CSV)
数据最小化采集字段最小集 + 存储期限策略
处理活动记录(DPA)记录处理目的、数据类别、保留期限
泄露通知(72h)审计日志 + 告警 + 泄露响应流程
// 数据主体权利接口示例
@RestController
@RequiredArgsConstructor
public class SubjectRightsController {
    private final UserDataService service;

    @GetMapping("/gdpr/users/{id}/data")        // 访问权
    public Mono<SubjectData> export(@PathVariable Long id, Principal principal) {
        // 校验当前主体是本人或授权
        authorize(principal.getName(), id);
        return service.exportSubjectData(id);
    }

    @DeleteMapping("/gdpr/users/{id}")          // 删除权
    public Mono<Void> erase(@PathVariable Long id, Principal principal) {
        authorize(principal.getName(), id);
        return service.anonymizeOrDelete(id);   // 审计记录 + 级联清理
    }
}

4.2 等保 2.0 三级要求映射

等保 2.0 三级是对互联网运营系统最常见的监管要求,覆盖安全物理/网络/主机/应用/数据五个层面。Java 应用侧相关要求:

等保要求项应用侧落地
身份鉴别(双因子)账号 + OTP/TOTP
访问控制(最小授权)RBAC + 对象级授权
安全审计审计日志保留 ≥ 6 个月
入侵防范WAF + 安全响应头 + 限流
数据完整性传输 TLS + 关键数据签名/哈希
数据保密性敏感字段加密存储(AES/GCM)
数据备份恢复定期备份 + 恢复演练
// 敏感数据加密存储示例(AES-GCM,带随机 IV)
public String encrypt(String plain, byte[] key) throws Exception {
    byte[] iv = new byte[12];
    SecureRandom.getInstanceStrong().nextBytes(iv);      // 安全随机 IV
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
    byte[] ct = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
    return Base64.getEncoder().encodeToString(iv) + "." + Base64.getEncoder().encodeToString(ct);
}
// 密钥由 KMS/Vault 托管,应用只引用别名,不硬编码

4.3 审计日志

// 统一审计切面:记录谁、何时、对什么资源、做了什么操作
@Aspect
@Component
public class AuditAspect {
    @Around("@annotation(audit)")
    public Object audit(ProceedingJoinPoint pjp, Audit audit) throws Throwable {
        String operator = SecurityContextHolder.getContext().getAuthentication().getName();
        String resource = audit.resource();
        long start = System.currentTimeMillis();
        try {
            Object result = pjp.proceed();
            auditService.record(operator, resource, "SUCCESS", null, start);
            return result;
        } catch (Throwable t) {
            auditService.record(operator, resource, "FAILED", t.getMessage(), start);
            throw t;
        }
    }
}

// 审计要点:不可篡改(Append-only)、可检索、保留期达标、与业务日志分离

5. 供应链安全

5.1 SBOM:软件物料清单

供应链安全的起点是「知道自己用了什么」。SBOM 列出应用的全部组件与版本:

# 生成 SBOM(CycloneDX 格式)
mvn org.cyclonedx:cyclonedx-maven-plugin:makeBom
# 产出 target/bom.json:全部依赖、版本、许可证、传递依赖

# 发布流水线强制校验:新依赖必须通过漏洞扫描才准入

5.2 依赖漏洞扫描

<!-- Maven 集成 OWASP Dependency-Check -->
<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <version>9.x.x</version>
    <configuration>
        <failBuildOnCVSS>7</failBuildOnCVSS>   <!-- CVSS ≥ 7 阻断发布 -->
        <assemblyAnalyzerEnabled>false</assemblyAnalyzerEnabled>
    </configuration>
</plugin>
mvn org.owasp:dependency-check-maven:check
# 产出 target/dependency-check-report.html
# 修复闭环:升级版本 → 复扫 → 通过后发布
工具特点
OWASP Dependency-Check开源,NVD 数据库,CI 可集成
Snyk商业,漏洞库更全,支持 IaC/容器扫描
GitHub Dependabot仓库内自动升级 PR
Sonatype OSS Index免费 API 查询

5.3 制品与镜像签名

# 镜像签名(cosign):发布前对镜像做签名,运行环境校验
cosign sign --key cosign.key <registry>/app:v1.2.3
cosign verify --key cosign.pub <registry>/app:v1.2.3

# 制品签名(Maven GPG)
mvn gpg:sign-and-deploy-file

# K8s 侧:配置镜像签名策略,只拉取签名合法的镜像

5.4 密钥管理

密钥类型存放位置禁止
数据库密码Vault / K8s Secret / KMS配置文件明文
API 密钥Vault 动态凭证硬编码进代码
JWT 签名密钥KMS 托管,定期轮换提交进 Git
TLS 证书证书管理平台过期后裸奔
# Spring Cloud 集成 Vault 获取配置与密钥
spring:
  cloud:
    vault:
      host: vault.example.com
      token: ${VAULT_TOKEN}          # 令牌由 CI 注入,非代码内
      kv:
        enabled: true
        backend: secret

6. 总结

主题核心要点
安全编码OWASP Top 10 对应 Java 落地、三原则、安全函数
注入防护参数化查询、白名单校验、禁用 XXE、类白名单反序列化
数据脱敏静态/动态脱敏、字段注解、日志脱敏、生命周期触点
合规GDPR 数据主体权利、等保三级映射、审计日志
供应链SBOM、依赖扫描、镜像签名、密钥托管

安全合规不是一次性的安全评审,而是贯穿编码、构建、发布、运行全生命周期的工程纪律。把安全编码变成肌肉记忆,把脱敏与审计变成基础设施,把供应链扫描变成发布门禁,你的系统才谈得上「安全可信」。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 云原生 Java:GraalVM 原生镜像、镜像瘦身与 Serverless
  2. WebFlux 响应式编程实战:Reactor 内核、响应式数据访问与选型
  3. Java 微服务治理深化:熔断限流降级、灰度发布与全链路压测