安全基础
这一课你将能做到
- 用 CIA 三原则(机密性、完整性、可用性)分析一个系统面临的风险
- 区分对称加密、非对称加密、哈希/消息摘要的用途,并正确存储用户密码
- 识别 SQL 注入、XSS、CSRF、越权访问四类常见 Web 漏洞及其防御手段
为什么要学
判题平台有用户身份、有用户上传的代码、有管理员后台、有开放接口——它几乎集齐了 Web 安全的全部攻击面。一次安全事故的代价远超普通 Bug:密码库被拖库、用户代码被窃取、后台被入侵植入恶意内容、接口被恶意刷爆导致服务不可用。
安全不是「上线前加个过滤器」,而是贯穿设计的思维方式:先假设输入是恶意的、网络是不可信的、权限会被试探。 本课建立安全的总框架(CIA)、密码学工具箱和与 Java Web 最相关的漏洞防御。它们也是第 8 课 TLS、第 11 课安全过滤器设计的落地基础。
先建立图像
CIA 三原则(信息安全的总目标)
机密性 Confidentiality 不该看的看不到 → 加密、权限控制
完整性 Integrity 数据没被篡改 → 哈希、签名、校验
可用性 Availability 该用的时候能用 → 防 DDoS、限流、容灾备份
(扩展:不可否认性 —— 做过的操作抵赖不掉 → 数字签名、审计日志)
密码学三件套
对称加密 同一把钥匙,快 AES 加密大量数据
非对称加密 公钥/私钥对 RSA/ECDSA 密钥协商、数字签名
哈希摘要 单向不可逆 SHA-256, 完整性校验、密码存储
bcrypt/Argon2(慢哈希+盐)
一次请求的攻防地图
用户输入 ──▶ 认证(你是谁) ──▶ 授权(你能干什么) ──▶ 参数处理(注入/XSS)
◀── 响应(防敏感信息泄漏/Cookie 安全属性) ──
跟着做
步骤 1:安全地存储密码(最重要的单一实践)
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordDemo {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
// 注册:绝不存明文,也不用 MD5/SHA-256 直接存(太快,易被彩虹表/撞库破解)
String raw = "MyPass_2026";
String encoded = encoder.encode(raw);
System.out.println("入库的密码哈希: " + encoded);
// 同一密码每次结果不同,因为哈希内含随机盐(salt),盐让相同密码哈希不同
// 登录:用 matches 校验,不自己解哈希(不可逆,也解不出来)
System.out.println(encoder.matches(raw, encoded)); // true
System.out.println(encoder.matches("wrong", encoded)); // false
}
}
关键认知:密码哈希必须加盐(防彩虹表)且故意设计得慢(bcrypt/Argon2/PBKDF2,让离线猜测成本极高)。数据库即使被拖库,攻击者也难以还原密码;同时要强制弱密码检测、登录失败限流,防范撞库。
步骤 2:用对三种密码学工具
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
public class CryptoToolbox {
// HMAC:带密钥的哈希,保证「消息来自持有密钥的一方且未被篡改」
// 典型用途:Webhook 回调签名、判题结果回调验签
static String hmacSha256(String data, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(raw);
}
public static void main(String[] args) throws Exception {
String body = "{\"submissionId\":1024,\"status\":\"AC\"}";
String sign = hmacSha256(body, "judge-callback-secret");
System.out.println("签名: " + sign);
}
}
选择规则要刻进脑子:
- 数据要能还原 → 加密;双方同一把钥匙(AES)或公私钥对(RSA/ECIES)
- 只需验证内容没改、来源可信 → 普通哈希(SHA-256)或带密钥的 HMAC
- 需要防抵赖 → 数字签名(私钥签、公钥验,JWT 的签名、软件发布签名)
- 哈希 ≠ 加密:哈希不可逆;不要发明自己的加密算法,也不要用 MD5/SHA-1(已不安全)
步骤 3:防住 SQL 注入
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class ProblemRepository {
private final JdbcTemplate jdbc;
public ProblemRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public Integer findByTitle(String title) {
// ❌ 拼接:title = "x' OR '1'='1" 即可绕过条件,甚至拖走全表
// String sql = "SELECT id FROM problems WHERE title='" + title + "'";
// ✅ 参数化:SQL 结构与参数分离,参数只被当作值,永远不会被解释成 SQL
String sql = "SELECT id FROM problems WHERE title = ?";
return jdbc.queryForObject(sql, Integer.class, title);
}
}
MyBatis 中用 #{}(预编译参数)而不是 ${}(字符串拼接,仅在必须动态指定表名/列名时使用且要白名单校验)。注入是 OWASP Top 10 的常客,根因永远是「把用户输入当成了代码执行」。
步骤 4:XSS、CSRF 与越权
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/submissions")
public class SubmissionController {
// 越权防御:水平越权=用户 A 查看用户 B 的记录;垂直越权=普通用户调用管理接口
// 不能只靠「前端不显示按钮」,每个接口都要在服务端校验资源归属
@GetMapping("/{id}")
@PreAuthorize("@submissionGuard.isOwner(#id) or hasRole('ADMIN')")
public String getOne(@PathVariable Long id) {
return "submission-" + id;
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void delete(@PathVariable Long id) {
// 管理操作必须有角色校验,并写审计日志(谁、何时、删了什么)
}
}
两类前端侧漏洞直觉:
- XSS(跨站脚本):用户输入被当作脚本在别人的页面执行。防御:输出到 HTML 时做转义(模板默认转义)、富文本用白名单过滤库(如 OWASP Java HTML Sanitizer)、Cookie 加 HttpOnly 防窃取
- CSRF(跨站请求伪造):诱导已登录用户在第三方站点发起请求。防御:敏感操作使用 CSRF Token/ SameSite Cookie;纯 JSON API + JWT 在 Header 中传递时天然不易受攻击,但仍要校验来源
讲清楚要点
- 认证 Authentication 与授权 Authorization 是两件事:先确认「你是谁」(登录、JWT/Session),再判断「你能做什么」(角色、权限、资源归属);顺序不能反,且授权必须在服务端每个敏感接口执行。
- 最小权限原则:用户、服务账号、数据库账号都只授予完成任务的最小权限;判题机执行用户代码用低权限账户 + 容器隔离,限制文件、网络、系统调用。
- 敏感数据要分级:密码、身份证、手机号、代码源码各有保护级别;日志里不要打印密码与完整身份证号,接口返回要脱敏,备份与传输同样要加密。
- 可用性攻击(DDoS/CC):流量型靠带宽与清洗服务,请求型靠限流、验证码、WAF;判题平台还要防「恶意提交死循环代码」耗尽判题产能(结合 MQ 排队与每用户提交频率限制)。
- 安全的纵深防御:网络层(防火墙/WAF)、主机层(补丁、最小化服务)、应用层(输入校验、参数化、CSRF/XSS 防护)、数据层(加密、备份、权限);任何单点被突破,其他层仍能兜底。
你可能会懵的地方
-
加盐有什么用?盐不是保密的吗? 盐不需要保密,它通常和哈希一起存储。盐的作用是让每个用户的哈希都不同:攻击者无法用一张预计算的彩虹表批量破解,必须逐个加盐猜测,成本大增。
-
JWT 安全吗?存在哪、被盗了怎么办? JWT 是带签名的令牌,防篡改但默认不加密(payload 仅 Base64,别放敏感信息)。被盗用的风险与 Cookie 相同:用 HTTPS、短有效期 + Refresh Token、必要时绑定设备/IP 特征,服务端维护吊销名单。
-
前端做了校验,后端还要做吗? 必须做。前端校验只为体验,攻击者可以绕过浏览器直接构造请求。所有安全校验的「生效位置」只能是服务端。
-
HTTPS 已经加密,是不是就不会被攻击了? HTTPS 解决传输链路的窃听与篡改,但它不防应用层漏洞:注入、越权、弱密码、XSS 照样成立。加密是安全的一环不是全部。
-
发现漏洞后该怎么做? 先止损(下线或限流受影响接口、封禁攻击源),再修复并验证,然后排查数据是否已被利用(查审计日志),最后复盘补充监控与测试;切忌悄悄修复不告知、不留记录。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 明文或 MD5 存密码 | 拖库即全部泄漏 | bcrypt/Argon2 加盐慢哈希 |
用 ${} 拼 MyBatis 参数 | SQL 注入 | #{} 参数化;动态结构用白名单 |
| 接口只按 ID 查询不校验归属 | 水平越权 | 服务端校验当前用户与资源关系 |
| 日志打印完整手机号/密码 | 日志泄漏即数据泄漏 | 脱敏打印;密码绝不入日志 |
| 自行设计加密/签名方案 | 几乎必然有缺陷 | 用经过审计的标准库与协议 |
卡点急救
- 怀疑被刷接口/撞库:立即对登录和提交接口加限流与验证码,封禁异常 IP/账号;核查失败日志的规模与来源分布。
- 发现注入点:先关闭或限流相关接口,确认数据访问账号权限(是否过大),修复为参数化后再上线,并审计历史异常请求。
- 权限疑似被绕过:复核 Security 配置是否对新接口生效(避免只保护了旧路径),用未登录/低权限账号实际回归一遍全部敏感接口。
面试常问
-
说一下对称加密、非对称加密和哈希的区别与使用场景。 对称(AES)一把钥匙、速度快,用于数据加密;非对称(RSA/ECDSA)公私钥对,用于密钥协商与数字签名;哈希(SHA 系列)单向定长,用于完整性与密码存储(密码必须加盐慢哈希)。TLS 是三者组合的标准案例。
-
如何防止 SQL 注入? 参数化查询(预编译)使代码与数据分离;最小权限账号;避免拼接表名/列名,必要时白名单校验;ORM 用安全占位符。可补充 WAF 只能作为补充,不能替代参数化。
-
什么是越权?水平和垂直越权怎么防? 越权是绕过授权访问未授权资源。水平越权访问同权限他人数据(用 ID 直接查),靠资源归属校验;垂直越权访问高权限功能,靠服务端角色/权限注解,前端隐藏按钮不算防御。
自测三问
- 用 CIA 给判题平台各列两个威胁与对应措施,能补上不可否认性更好。
- 用户登录全链路中,密码在前端、传输、存储、校验四个环节分别怎么处理?
- XSS 和 CSRF 的攻击方向有何不同?Cookie 的 HttpOnly 与 SameSite 分别防谁?
小结一句话
安全以 CIA 为目标、以「输入皆恶意、权限服务端兜底、密码加盐慢哈希、系统永不自创密码学」为基本纪律,靠纵深防御把单点失陷的风险层层稀释。
延伸
- OWASP Top 10 官方项目(2021 版)——Web 应用最关键风险清单与防护指南
- Spring Security 官方参考文档——认证授权架构与配置的权威资料
- NIST 数字身份指南(SP 800-63B)——密码与身份验证的国际权威建议
- 下一课:设计模式与 UML——掌握了安全与设计的原则,下一课学习如何把良好结构固化为可复用的模式:GoF 经典模式与 UML 表达。