应用安全
这一课你将能做到
- 说清 CIA 三元组(机密性、完整性、可用性),并能给本站每个组件对应一个 CIA 控制
- 识别 OWASP Top 10 中的前 5 项,写出对应 Spring Boot 的防御代码(SQL 注入、XSS、CSRF、不安全反序列化、安全配置错误)
- 用真实 Payload 在本站测试注入类漏洞,确认修复后的代码能拦截攻击
为什么要学
应用是最终暴露给攻击者的界面:你的 REST API、管理后台、第三方回调。无论防火墙多厚、TLS 多强,一个 Statement.execute(sql) 拼接的 SQL 注入就能让你全库裸奔。Log4Shell 也是应用层漏洞——不是网络配置问题。
人话:OWASP Top 10 是过去十年全球 web 应用漏洞的排行榜,前 5 条几乎全是输入校验问题。核心哲学只有一个——永远不要信任用户输入:表单、URL 参数、请求头、文件上传、第三方回调,全是攻击面。
先建立图像
CIA 三元组与本站映射:
机密性(C): HTTPS / AES 加密手机号 / JWT 验签
│
完整性(I): 密码哈希不可改 / 审计日志不可删 / 参数签名
│
可用性(A): Nginx 限流 / Redis 过期策略 / 数据库连接池上限
一次 SQL 注入的真实路径:
用户输入: ' OR '1'='1' --
│
拼接 SQL: SELECT * FROM users WHERE username = '' OR '1'='1' --'
│
结果: 条件永远为真,返回全部用户(包括管理员)
跟着做
步骤 1:SQL 注入——从「拼接」到「参数化」
❌ 错误示范(永不这样写):
public User findByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
return jdbcTemplate.queryForObject(sql, User.class); // 灾难
}
✅ 正确做法(PreparedStatement / JPA):
// JDBC
public User findByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = ?";
return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), username);
}
// JPA / Spring Data
public interface UserRepository extends JpaRepository<User, Long> {
// 自动参数化,不要用 @Query 拼接
Optional<User> findByUsername(String username);
}
手动构造注入 Payload 验证:
curl "https://coderoadmap.cn/api/users?username=' OR '1'='1' --"
# 正确防御时:返回空或 400,绝不应返回列表
步骤 2:XSS——输出编码 + 内容安全策略
❌ 错误:直接回显用户输入:
@GetMapping("/search")
public String search(@RequestParam String q) {
return "<div>搜索 " + q + " 的结果</div>"; // 用户传 <script>alert(1)</script> 就执行
}
✅ 正确做法:
// 模板层(Thymeleaf 默认 HTML 编码,安全)
// 如果用前端分离(Vue/React),注意 SSR 时也要编码
// API 层直接返回 JSON,不要返回 HTML
@GetMapping("/search")
public SearchResult search(@RequestParam String q) {
// 返回 POJO,Spring 自动 JSON 序列化,特殊字符被转义
return searchService.find(q);
}
Nginx 加 CSP(Content Security Policy)响应头:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;
步骤 3:CSRF——无状态 API 关,有状态表单开
纯 JWT + 无 Cookie 的前后端分离接口:不需要 CSRF,因为攻击者发不出自定义 Authorization: Bearer <token> 请求(跨域限制+无 Cookie)。
如果有状态 Session 或混合登录(Form Login),必须开 CSRF:
// 前端发送 POST /login 时,Spring Security 默认要求 _csrf token
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.requireCsrfProtectionMatcher(
request -> !request.getRequestURI().startsWith("/api/") // 除 /api 外都要 token
)
);
步骤 4:不安全反序列化——白名单
ObjectInputStream ois = new ObjectInputStream(req.getInputStream());
Object obj = ois.readObject(); // 攻击者构造恶意对象链=远程代码执行
✅ 正确做法(用 JSON 反序列化代替原生 Java 序列化):
// 用 Jackson,默认不能反序列化任意类
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(json, User.class);
// 如果必须用 ObjectInputStream,限制白名单
class SafeObjectInputStream extends ObjectInputStream {
@Override protected Class<?> resolveClass(ObjectStreamClass desc) {
if (!desc.getName().startsWith("cn.coderoadmap.")) {
throw new SecurityException("只允许反序列化白名单类");
}
return super.resolveClass(desc);
}
}
步骤 5:安全配置错误——逐项检查
// application.yml 安全基线
server:
error:
include-message: never # 生产环境不暴露异常堆栈
include-stacktrace: never
servlet:
session:
cookie:
http-only: true # 防 JS 读 cookie
secure: true # HTTPS 下才传 cookie
same-site: strict # 防 CSRF
spring:
mvc:
throw-exception-if-no-handler-found: false # 避免路径枚举
add_header X-Frame-Options "DENY" always; # 防点击劫持
add_header X-Content-Type-Options "nosniff" always; # 防 MIME 嗅探
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always;
步骤 6:安全响应头一键验证
curl -I https://coderoadmap.cn
# 逐条检查输出里的:Strict-Transport-Security、X-Frame-Options、
# X-Content-Type-Options、Content-Security-Policy
讲清楚要点
-
输入校验是第一防线:无论前端有没有校验,后端永远再校验一次。不为性能牺牲安全——参数校验在请求入口处做,成本远低于一次 SQL 注入后的数据恢复。
-
参数化查询是 SQL 注入的银弹:不是手动加转义(迟早漏掉一个),是让数据库驱动自己处理参数。JPA、MyBatis #{}、JDBC ? 都是参数化。
-
XSS 的本质是输出问题:不是输入不能含尖括号,而是输出到 HTML 时要编码。前后端分离架构天然更安全,因为 API 返回 JSON 而非 HTML。但要注意 SSR、富文本编辑器的例外处理。
-
错误信息越模糊越安全:生产环境不要把 java.lang.NullPointerException at UserService.java:45 返回给客户端。打日志给运维看,给客户端的只给"操作失败,请重试"+traceId。
-
安全响应头是零成本的补丁:几个 add_header 就能挡住点击劫持、MIME 嗅探、降级攻击。配完测一遍,以后不用管。
你可能会懵的地方
-
MyBatis 里 ${} 和 #{} 有什么区别?
#{} 是参数化(安全),${} 是字符串替换(危险,只用于动态表名/列名且必须白名单校验)。绝不把用户输入直接放进 ${}。
-
JWT 接口关了 CSRF,那 Cookie 凭据怎么办?
如果你的 API 同时靠 Session Cookie 认证,CSRF 必须开。纯 JWT(存在 localStorage、Header 传递)天然免疫 CSRF——因为跨域请求发不出自定义 Header(CORS preflight 拦截),且没 Cookie 可利用。
-
富文本编辑器(允许 HTML)怎么防 XSS?
用成熟库(如 JSoup)做白名单过滤:只允许 <p><b><i><a> 等安全标签和 href 属性。不要用黑名单(你不可能穷举所有恶意标签)。
-
业务逻辑漏洞算 OWASP Top 10 吗?
2021 版新增 A01 Broken Access Control,涵盖的就是 IDOR、越权。纯注入类漏洞已退居第二,说明现代框架已经大幅减少了 SQL 注入,但业务逻辑校验仍是开发者的直接责任。
-
序列化攻击只影响 Java 吗?
不是。Python pickle、PHP unserialize、Node vm 都有类似问题。原则一致:永远不要反序列化不受信任来源的数据;必须用就加白名单;优先用 JSON/Protobuf 等不含可执行语义的格式。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 字符串拼接 SQL | SQL 注入 | PreparedStatement / #{} |
| 直接回显用户输入到 HTML | XSS | 模板编码或返回 JSON |
ObjectInputStream.readObject 任意来源 | RCE | JSON 反序列化 + 白名单 |
| 生产环境返回详细异常堆栈 | 信息泄漏+路径枚举 | include-message/stacktrace=never |
| Nginx 不加安全响应头 | 点击劫持、降级、MIME 嗅探 | X-Frame-Options / HSTS / CSP |
| 信任前端校验 | 绕过太容易 | 后端入口再次校验所有输入 |
卡点急救
-
开了 CSP 后页面样式/脚本不加载:CSP 默认拒绝内联脚本和外部 CDN。解法:开发环境放宽到 script-src 'self' 'unsafe-inline' 'unsafe-eval',生产严格化。把外部资源加入 script-src、style-src 白名单。
-
MyBatis #{} 处理不了 IN (...) 列表:参数化查询不支持列表展开。解法:用 foreach 动态生成 ? 占位符(MyBatis 自动处理),不是把用户输入拼成字符串放进 IN (...)。
-
Spring Boot 生产环境还暴露 Whitelabel Error Page:关掉它,配置自定义错误页,或设 server.error.whitelabel.enabled=false,避免错误信息泄漏和路径枚举。
面试常问
-
SQL 注入原理和防御?
原理:用户输入被拼接到 SQL 语句中,改变语法语义。防御:参数化查询(PreparedStatement);ORM/JPA 天然安全;不使用字符串拼接。特殊场景动态表名列名用白名单校验后拼接。
-
XSS 类型和防御?
存储型(存入数据库后展示给所有用户)、反射型(URL 参数直接回显)、DOM 型(前端 JS 不当地操作 DOM)。防御:输出编码(HTML、JS、URL、CSS 上下文不同编码方式)、CSP 限制脚本来源、HttpOnly Cookie。前后端分离+JSON API 天然大幅降低风险。
-
生产环境为什么不该暴露堆栈?
信息泄漏:框架版本、代码路径、内部类名帮助攻击者定制 payload;路径枚举:从报错能推断存在/不存在的 API。错误处理原则:给用户的 message 模糊,给日志的 detail 完整,靠 traceId 关联。
自测三问
- MyBatis
#{} 和 ${} 的区别是什么?哪个安全?
- 为什么前后端分离的 JWT 架构不需要 CSRF 防护?
- 生产环境
server.error 三项配置应该分别设成什么?为什么?
小结一句话
应用安全就是一句话——用户输入永不信任:SQL 参数化、HTML 编码、反序列化白名单、错误信息模糊化,再加一套安全响应头兜底。
延伸