1. 安全编码概述:为什么需要安全编码、常见安全威胁模型(STRIDE)、安全编码基本原则
1.1 我们为什么要谈安全编码?
说实话,我见过太多开发人员觉得安全是安全团队的事。我自己刚入行那会儿也这么想——把功能做出来就行了,安全?那是运维和防火墙的事。
直到有一次,我参与的一个电商项目上线第三天就被拖了库。用户信息、订单数据全被 dumped 到了暗网上。老板脸都绿了。后来一查,问题出在哪?就是一个简单的 SQL 注入。代码里直接把用户输入拼到了查询语句里。嗯,就是这么低级的问题,让整个公司赔了上千万。
从那以后,我彻底明白了:安全不是附加功能,而是代码质量的一部分。你写的每一行代码,要么在保护用户,要么在给攻击者开门。
核心观点:安全编码不是「额外工作」,而是「本职工作」。一个漏洞的平均修复成本,在开发阶段发现是 100 元,到了生产环境就是 10 万元起步。
你想想看,现在哪个应用不处理用户数据?哪个系统不联网?只要你的代码暴露在网络上,就有人盯着。不是吓唬你,我见过太多「小公司没事」的心态,结果被爬虫、被薅羊毛、被勒索——说白了,黑客才不管你是大厂还是小团队。
1.2 常见安全威胁模型:STRIDE
聊威胁模型之前,我先问个问题:你知道攻击者最常用的套路是什么吗?
我个人习惯用微软的 STRIDE 模型来梳理威胁。这个模型把安全威胁分成六类,每一类对应一个你代码里可能存在的弱点。来,我一个个说。
| 威胁类型 | 英文 | 什么意思? | 代码里常见表现 |
|---|---|---|---|
| S - 身份欺骗 | Spoofing | 冒充别人身份 | Session 固定、弱密码、Token 泄露 |
| T - 篡改 | Tampering | 修改数据或代码 | 参数校验缺失、未签名数据 |
| R - 抵赖 | Repudiation | 做了不承认 | 缺少审计日志、日志不完整 |
| I - 信息泄露 | Information Disclosure | 不该看的被看到了 | 错误信息暴露堆栈、敏感字段未脱敏 |
| D - 拒绝服务 | Denial of Service | 让系统不可用 | 未限流、大对象未释放、正则回溯 |
| E - 权限提升 | Elevation of Privilege | 拿到不该有的权限 | 越权访问、未校验角色 |
我在项目中遇到过最典型的例子是「权限提升」。一个后台管理系统,用户 A 能看到用户 B 的订单。为什么?因为接口只校验了「是否登录」,没校验「是否属于当前用户」。你想想看,这多可怕?
我的小技巧:每次设计接口时,拿 STRIDE 过一遍。每个字母问自己一句:「这个接口会不会被 XXX 攻击?」花不了 5 分钟,但能堵住 80% 的常见漏洞。
1.3 安全编码的基本原则
好,理论说完了,咱们来点实在的。安全编码到底怎么落地?我总结了五条原则,都是血泪换来的。
原则一:最小权限
说白了,就是「够用就行」。你的代码不需要 root 权限,那就别用。你的数据库连接不需要 DDL 权限,那就只给 CRUD。我曾经见过一个项目,应用账号用的是数据库的超级管理员——结果一个 SQL 注入,整个库都被删了。
// ❌ 错误做法:使用高权限账号
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/db", "root", "password");
// ✅ 正确做法:只给必要的权限
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/db", "app_user", "app_password");
// 数据库里:GRANT SELECT, INSERT, UPDATE, DELETE ON db.* TO 'app_user';
原则二:纵深防御
别指望一道防线挡住所有攻击。我习惯在每一层都加防护:前端校验、后端校验、数据库约束、日志审计。就像你家门,有锁还不够,再加个猫眼、装个监控。攻击者突破一层,还有下一层等着他。
原则三:默认安全
系统的默认配置应该是安全的。别让用户自己去「开启安全功能」。我见过太多框架默认关闭了 CSRF 保护,结果开发人员忘了打开——嗯,漏洞就这么来了。
避坑指南:我曾经接手过一个项目,默认配置里 session 超时时间是 24 小时。用户在公司电脑上登录后忘了退出,被同事拿去乱搞。后来我改成 30 分钟,敏感操作还要二次验证。记住:默认值一定要安全。
原则四:永不信任用户输入
这是安全编码的黄金法则。所有来自用户的东西——表单、URL 参数、Cookie、HTTP 头——都可能是恶意构造的。你想想看,攻击者会乖乖按你的规则来吗?
// ❌ 错误做法:直接拼接用户输入
String query = "SELECT * FROM users WHERE id = " + request.getParameter("id");
// ✅ 正确做法:使用参数化查询
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE id = ?");
stmt.setInt(1, Integer.parseInt(request.getParameter("id")));
原则五:失败安全
系统出错时,应该默认拒绝访问,而不是默认放行。我见过一个认证模块,异常处理写成了:
// ❌ 错误做法:异常时默认通过
try {
// 验证用户身份
} catch (Exception e) {
// 出错了?那就让他过吧
return true;
}
// ✅ 正确做法:异常时默认拒绝
try {
// 验证用户身份
return isValid;
} catch (Exception e) {
log.error("认证异常,拒绝访问", e);
return false;
}
你看,一个简单的 catch 块,就能决定系统是安全还是沦陷。
1.4 写在最后
安全编码不是什么高深莫测的东西。说白了,就是养成好习惯。每次写代码前,多问自己一句:「这段代码如果被攻击者盯上,会出什么问题?」
我个人习惯在代码审查时专门过一遍安全 checklist。STRIDE 模型、最小权限、输入校验——这些不是理论,是每天都要用的工具。你坚持用上一个月,就会发现:原来很多漏洞,根本到不了生产环境。
好,这一章就聊到这儿。下一章咱们深入聊聊「输入验证与输出编码」,这是安全编码里最基础也最重要的部分。