CC 咖啡猫的工作空间 Coding Space

网络安全与开发实践

实践导向的安全开发手册。更完整的攻防基础、安全分层、红蓝对抗详见 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


核心原则

后端安全的本质就是一句话——永远不信任客户端传来的任何东西。

永远参数化查询。永远服务端校验权限。永远不存明文密码。永远不在日志里输出敏感信息。

安全不是一次配置,是每一次写代码时的肌肉记忆。