网络安全与开发实践
实践导向的安全开发手册。更完整的攻防基础、安全分层、红蓝对抗详见
z-软件开发安全与攻防基础.md。
〇、快速建立认知
安全不只等于"防黑客"
软件开发安全可以简化为四个层面——每个层面出事的表现不同,应对也不同:
传输层安全 —— 数据在"路上"是否安全 → HTTPS、证书
应用层安全 —— 代码有没有漏洞 → 注入/越权/XSS 防护 ← 后端主战场
数据层安全 —— 存的数据会不会泄露 → 加密/脱敏/日志安全
业务层安全 —— 业务逻辑有没有被钻空子 → 薅羊毛/刷单/支付篡改
更完整的 8 层安全分层(含供应链安全、基础设施安全、合规等)见
z-软件开发安全与攻防基础.md。
核心原则
永远不信任客户端传来的任何东西。
用户的输入、Token、上传的文件、请求的 Header……每一样都要在后端再做一次校验。前端校验只是用户体验,不是安全措施。
一、攻击面速览(按模式分组)
不要逐个背攻击类型。把它们按攻击模式分组,理解一组就防住一类。
1.1 注入类(Injection)—— 用户输入被当成代码执行
| 攻击 | 一句话 | Java 重灾区 |
|---|---|---|
| SQL 注入 | 用户输入拼进 SQL,改了查询逻辑 | ✅ |
| 命令注入 | 用户输入拼进系统命令 | ⚠️ |
| LDAP 注入 | 用户输入拼进 LDAP 查询 | ⚠️ |
| 日志注入 | 用户在输入里塞换行符,伪造日志 | ✅ |
共同特征:开发者把用户输入和指令字符串拼接在一起。
统一防护思路:永远不拼接,用参数化/预编译。
// ❌ 拼接(危险)
String sql = "SELECT * FROM users WHERE name = '" + username + "'";
// ✅ 参数化(安全)
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
1.2 跨站类(XSS + CSRF)—— 浏览器端的信任滥用
| 攻击 | 一句话 | 防护关键词 |
|---|---|---|
| 反射型 XSS | 恶意脚本藏在 URL 参数里 | 输出编码 |
| 存储型 XSS | 恶意脚本存在数据库,所有访问者中招 | 入库前清洗 |
| DOM 型 XSS | 纯前端,JS 操作 DOM 时被注入 | 前端安全 API |
| CSRF | 第三方网站冒用你的登录态发请求 | CSRF Token / SameSite |
统一防护思路:
- XSS → 对输出到 HTML/JS 的内容做编码,不信任任何用户输入
- CSRF → 校验请求来源(Referer/Origin + Token),敏感操作加二次确认
1.3 请求伪造类(SSRF)—— 把服务器当跳板
核心问题:用户传一个 URL 让你去请求,你照做了,结果请求到了内网。
// 危险:用户传什么 URL 就请求什么
String url = request.getParameter("callbackUrl");
restTemplate.getForObject(url, String.class); // 可能打到内网
// 安全:白名单 + 内网 IP 黑名单
private boolean isSafeUrl(String url) {
// 1. 协议白名单:只允许 http/https
// 2. 域名白名单:只允许已备案的外部域名
// 3. 禁止访问内网 IP 段:10.x, 172.16-31.x, 192.168.x, 127.x
}
1.4 文件与序列化类
| 攻击 | 一句话 | 防护 |
|---|---|---|
| 文件上传漏洞 | 上传 .jsp 文件然后直接访问执行 |
校验真实文件类型(魔数),不信任扩展名;存在 OSS 非本地 |
| 反序列化漏洞 | 恶意序列化数据在反序列化时执行代码 | 不用 Java 原生序列化接收外部数据;用 JSON/Protobuf |
| 路径穿越 | ../../etc/passwd 读取系统文件 |
拼接路径后 normalize 并校验是否在允许目录内 |
1.5 业务逻辑类
这类漏洞不是代码 Bug,而是流程上被钻了空子:
| 问题 | 表现 | 防护思路 |
|---|---|---|
| 越权(IDOR) | 改 URL 里的 ID 看到别人的数据 | 每个接口校验"当前用户是否有权访问这个资源" |
| 薅羊毛/刷单 | 脚本批量注册领优惠券 | 注册/领券接口加验证码 + 频率限制 |
| 支付金额篡改 | 前端改金额传给后端 | 后端从数据库查价格,不用前端传来的金额 |
| 短信轰炸 | 一个手机号反复触发短信接口 | 同一手机号/同一 IP 加频率限制 |
1.6 传输与配置类
| 问题 | 表现 | 防护 |
|---|---|---|
| 中间人攻击 | HTTP 明文传输被窃听 | 全站 HTTPS + HSTS |
| 敏感数据泄露 | 日志/响应/错误信息把不该暴露的露出去了 | 日志脱敏、统一错误响应 |
| 安全配置错误 | 默认密码不修改、调试接口暴露到公网 | 生产配置 checklist |
二、后端开发安全实践清单(按优先级)
以下是后端开发者需要掌握的防护清单。按优先级排列——先守住第一优先级的,再逐步完善后面。
🔴 第一优先级:不做好就是事故
2.1 SQL 注入防护
这是后端安全的第一道防线。只要用参数化查询,SQL 注入就防住了 95%。
// ✅ Spring JDBC PreparedStatement
jdbcTemplate.queryForList(
"SELECT * FROM users WHERE username = ?", username);
// ✅ JPA / Hibernate(参数绑定)
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
// ✅ MyBatis(#{} 是参数化,${} 是拼接——永远用 #{})
@Select("SELECT * FROM users WHERE username = #{username}")
User findByUsername(String username);
// ⚠️ 动态排序/分组场景:ORDER BY / GROUP BY 无法用参数化
// 必须做白名单校验
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "username", "create_time");
public List<User> getUsersByOrder(String orderBy) {
if (!ALLOWED_COLUMNS.contains(orderBy)) {
throw new IllegalArgumentException("Invalid column: " + orderBy);
}
return mapper.getUsersByOrder(orderBy);
}
2.2 认证安全
// ✅ 密码存储:永远用 bcrypt 哈希,永远不存明文
String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12));
// ✅ 密码校验:用恒定时间比较(防时序攻击)
BCrypt.checkpw(inputPassword, storedHash); // BCrypt 内置恒定时间比较
// ✅ JWT 校验:验证签名 + 验证过期 + 验证签发者
Claims claims = Jwts.parser()
.verifyWith(secretKey) // 1. 验证签名(不信任未签名的 token)
.build()
.parseSignedClaims(token)
.getPayload();
// 2. 再校验过期时间、issuer 等
密码策略:
- 用 bcrypt / scrypt / Argon2,不要用 MD5 / SHA(太快,容易被暴力破解)
- bcrypt cost factor 建议 10-12(平衡安全性和性能)
- 不要把密码明文打印到日志里
2.3 越权控制(IDOR)
这是最容易被忽视但后果严重的漏洞——每个接口都要校验"当前用户有没有权限操作这个资源"。
// ❌ 危险:只校验了登录,没校验"是不是他自己的数据"
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
return orderService.getById(orderId); // 任何人登录后都能看任何订单
}
// ✅ 安全:同时校验资源归属
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
Long currentUserId = SecurityContextHolder.getCurrentUserId();
Order order = orderService.getById(orderId);
if (!order.getUserId().equals(currentUserId)) {
throw new AccessDeniedException("无权访问此订单");
}
return order;
}
实践要点:
- 不要在 URL 里用自增 ID 暴露数据量——用 UUID 或 HashId
- 写一个统一的
@CheckPermission注解 + AOP,避免每个接口手动写校验 - 批量操作也要逐条校验权限
2.4 关键数据加密
| 数据类型 | 加密方式 | 说明 |
|---|---|---|
| 密码 | 单向哈希(bcrypt) | 不需要还原,只需校验 |
| 手机号/身份证 | 可逆加密(AES) | 需要还原显示,但数据库泄露不能直接读 |
| 银行卡号/密钥 | 可逆加密(AES) + 字段级加密 | 最高敏感级别 |
// 敏感字段用 @Convert 自动加解密
@Entity
public class User {
@Convert(converter = AESEncryptConverter.class)
private String phone; // 存数据库时自动加密,读出来自动解密
}
2.5 HTTPS 强制
# Nginx 强制 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
add_header Strict-Transport-Security "max-age=63072000" always; # HSTS
🟡 第二优先级:容易被忽视但后果严重
2.6 日志脱敏
日志里不能出现明文密码、Token、身份证号、银行卡号。
// ✅ 用 logback 的 %replace 或自定义 Converter 统一脱敏
// logback.xml
<conversionRule conversionWord="mask"
converterClass="com.example.MaskingConverter" />
// 或在日志框架的 appender 层做统一拦截
public class SensitiveDataMasker {
// 手机号:138****1234
public static String maskPhone(String phone) {
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
// 身份证:110101****1234
public static String maskIdCard(String idCard) {
return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1****$2");
}
}
实践要点:
- 不要在
log.info("用户登录:" + user)中把整个 user 对象打出来(toString 可能包含密码) - 全局异常处理器的返回信息不要包含 SQL 语句、堆栈、内部 IP
- 生产日志级别至少为 INFO,DEBUG 只在排查问题时临时开启
2.7 CSRF 防护
// Spring Security 默认开启 CSRF 防护(基于 Cookie 的会话)
// 如果是前后端分离 + JWT(无 Cookie),CSRF 风险较低
// 敏感操作额外校验
@PostMapping("/api/transfer")
public Result transfer(@RequestBody TransferRequest req,
@RequestHeader("X-CSRF-Token") String csrfToken) {
// 校验 CSRF Token
csrfService.validate(csrfToken);
// ...
}
2.8 SSRF 防护
// 对用户提供的 URL 做三层校验
private boolean isValidUrl(String urlStr) {
URL url = new URL(urlStr);
// 第一层:只允许 http/https
if (!url.getProtocol().matches("https?")) return false;
// 第二层:解析域名对应的 IP
InetAddress addr = InetAddress.getByName(url.getHost());
// 第三层:禁止访问内网 IP
if (addr.isSiteLocalAddress() || addr.isLoopbackAddress()) return false;
return true;
}
2.9 文件上传校验
// 第一层:校验文件大小
if (file.getSize() > MAX_SIZE) throw new FileTooLargeException();
// 第二层:校验文件魔数(不信任扩展名)
String magic = Files.probeContentType(filePath);
if (!ALLOWED_MIME_TYPES.contains(magic)) throw new InvalidFileTypeException();
// 第三层:重命名 + 存储到 OSS(不存本地 Web 目录)
String newName = UUID.randomUUID() + getExtension(originalName);
ossClient.upload(BUCKET_NAME, newName, file.getInputStream());
// 第四层(可选):病毒扫描
antivirusService.scan(filePath);
2.10 接口限流
// 敏感接口(登录/短信/注册)必须加限流
@PostMapping("/api/sms/send")
@RateLimit(key = "#phone", limit = 5, window = 60) // 同一手机号1分钟最多5次
public Result sendSms(@RequestParam String phone) {
// ...
}
// 或用 Redis + Lua 实现滑动窗口限流
String lua = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1
end
return 0
""";
🟢 第三优先级:锦上添花
| 安全点 | 做法 | 一句话 |
|---|---|---|
| WAF | 在网关层拦截常见攻击特征(云厂商 WAF 或 ModSecurity) | 不用后端自写防护规则 |
| 依赖扫描 | mvn dependency-check:check / ./gradlew dependencyCheckAnalyze 或 GitHub Dependabot |
自动发现第三方库已知漏洞 |
| CORS 配置 | Access-Control-Allow-Origin 不用 *,明确列出允许的域名 |
防止其他域名跨域调用你的 API |
| 请求签名 | 对外 API 加 HMAC 签名(时间戳 + Nonce + 签名),防篡改 + 防重放 | 开放 API 必做 |
三、安全开发流程实践(SDL 精简版)
不要被"SDL"这个名字吓到。核心就是在开发流程的每个节点加一道安全检查。
需求阶段
- 明确这个功能有没有敏感操作(涉及钱、个人信息、权限变更)?
- 敏感操作 → 需要安全设计评审
设计阶段
- 画出这个功能的接口权限矩阵:谁(角色)能访问哪些接口?
- 明确敏感数据流向:哪些参数需要加密、哪些需要脱敏
开发阶段
- 遵循上面的**🔴 第一优先级清单**(SQL 注入、认证、越权、加密、HTTPS)
- Code Review 增加安全检查点:是否有拼接 SQL?是否有未校验权限的接口?
测试阶段
- SAST(静态扫描):SonarQube + SpotBugs(FindSecBugs 插件),CI 中自动跑
- DAST(动态扫描):OWASP ZAP 对测试环境做一轮自动扫描
- 依赖扫描:
mvn dependency-check:check或在 CI 中集成
部署阶段
- 生产配置 checklist:HTTPS 是否开启?调试接口是否关闭?默认密码是否已改?
- 部署账户最小权限(不要用 root 跑应用)
运维阶段
- 监控异常:登录失败率突增(可能撞库)、接口响应时间暴涨(可能被攻击)
- 告警:单 IP 高频访问 → 可能是在扫描漏洞
- 定期复查依赖漏洞 + 更新补丁
四、工具速查
| 阶段 | 工具 | 用途 |
|---|---|---|
| 开发 | SonarLint(IDE 插件) | 写代码时实时提示安全问题 |
| 开发 | SpotBugs + FindSecBugs | Java 代码静态安全分析 |
| 构建 | OWASP Dependency-Check | Maven/Gradle 依赖漏洞扫描 |
| 构建 | Snyk / GitHub Dependabot | 依赖漏洞持续监控 |
| 测试 | OWASP ZAP | 免费 Web 漏洞扫描器 |
| 测试 | Burp Suite Community | Web 安全手工测试 |
| 运维 | ModSecurity / 云 WAF | Web 应用防火墙 |
| 运维 | ELK Stack + 告警规则 | 安全日志分析与告警 |
更多安全资源(OWASP 学习平台、书籍推荐)见
z-软件开发安全与攻防基础.md。
核心原则
后端安全的本质就是一句话——永远不信任客户端传来的任何东西。
永远参数化查询。永远服务端校验权限。永远不存明文密码。永远不在日志里输出敏感信息。
安全不是一次配置,是每一次写代码时的肌肉记忆。