安全不是某个安全团队的专属工作,而是每一个后端工程师的编码习惯。在数据泄露事故频发、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、依赖扫描、镜像签名、密钥托管 |
安全合规不是一次性的安全评审,而是贯穿编码、构建、发布、运行全生命周期的工程纪律。把安全编码变成肌肉记忆,把脱敏与审计变成基础设施,把供应链扫描变成发布门禁,你的系统才谈得上「安全可信」。
延伸阅读
- Spring Security Web 安全纵深防御 — CSRF/CORS/XSS/CSP 响应头
- Java 安全认证与授权 — OAuth2/JWT/RBAC 认证体系
- Java 代码质量:SonarQube、Checkstyle 与 SpotBugs — 静态分析门禁
- 日志框架、MDC 与分布式链路追踪 — 日志脱敏与审计链路
- Bean Validation 与 Hibernate Validator 数据校验精要 — 声明式输入校验
- Java 容器化最佳实践与 Kubernetes 部署 — 镜像签名与 Secret 管理
- 信息安全与云安全 — 纵深防御体系全貌
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。