后端安全
前端安全管「浏览器里怎么防」,这篇管服务端怎么防 —— 因为真正的安全边界在服务端,前端只是第一道。 原则先记住:永远不信任客户端输入。
一、认证 vs 授权
| 认证(Authentication) | 授权(Authorization) | |
|---|---|---|
| 回答 | 「你是谁」 | 「你能干什么」 |
| 手段 | 密码、OAuth、SSO、短信 | 角色、权限、ACL、RBAC |
| 失败码 | 401 Unauthorized | 403 Forbidden |
常见混淆:登录 = 认证;「这个用户能不能删这个订单」 = 授权。很多越权漏洞就是只做了认证、没做对象级授权。
二、密码怎么存
java
// ❌ 明文 / MD5 / SHA1(MD5 和 SHA1 都可被彩虹表秒破)
// ✅ 加盐 + 慢哈希
String hash = BCrypt.hashpw(password, BCrypt.gensalt(12));
boolean ok = BCrypt.checkpw(input, hash);// ❌ 明文 / MD5 / SHA1(MD5 和 SHA1 都可被彩虹表秒破)
// ✅ 加盐 + 慢哈希
String hash = BCrypt.hashpw(password, BCrypt.gensalt(12));
boolean ok = BCrypt.checkpw(input, hash);| 算法 | 特点 |
|---|---|
| bcrypt | 老牌,成本因子可调 |
| argon2id | 更现代,抗 GPU 更好(新项目首选) |
| scrypt | 内存开销大 |
| PBKDF2 | FIPS 合规场景 |
铁律:
- 必须加盐(每个用户独立随机盐),否则同密码有同哈希,彩虹表一查一个准;
- 用慢哈希(故意慢,拖垮暴力破解);
- 密码哈希永不落日志。
三、JWT / Session / OAuth2
| 方案 | 适合 | 关键坑 |
|---|---|---|
| Session + Cookie | 传统 Web | 横向扩展要共享 session |
| JWT | 前后端分离、多端 | 无法主动失效,只能短 TTL + 刷新令牌 |
| OAuth2 / OIDC | 第三方授权、单点登录 | 授权码模式最安全,别用隐式模式 |
JWT 的三个高频坑
- 不是加密:payload 只是 base64,谁都能解码 —— 别放敏感信息;
- 无法登出:发出去的 token 在过期前一直有效,靠黑名单/短 TTL 缓解;
- 算法混淆攻击:校验时强制指定算法(
alg只接受 HS256/RS256),别信 header 里写的。
OAuth2 四种模式
| 模式 | 场景 |
|---|---|
| 授权码(带 PKCE) | 有后端的 Web/App(最安全) |
| 客户端凭证 | 服务间调用 |
| 密码模式 | 已废弃,别用 |
| 隐式模式 | 已废弃,别用 |
四、加密与哈希
| 用途 | 代表 | 可逆 | |
|---|---|---|---|
| 哈希 | 校验、存密码 | SHA-256、argon2 | ❌ |
| 对称加密 | 数据加密(快) | AES-GCM | ✅ |
| 非对称加密 | 密钥交换、签名 | RSA、ECDSA | ✅ |
| HMAC | 消息认证(防篡改) | HMAC-SHA256 | — |
关键选择:
- 数据加密用 AES-GCM(认证加密,同时防篡改),别用裸 AES-ECB(会被重放/拼接攻击);
- 签名用 HMAC(对称)或 RSA/ECDSA(非对称);
- 密钥别写进代码/仓库:放 KMS、环境变量、Secret 管理。
五、常见攻击与防御
SQL 注入
java
// ❌ 字符串拼接:name = "1' OR '1'='1"
String sql = "SELECT * FROM user WHERE name = '" + name + "'";
// ✅ 参数化查询(PreparedStatement / MyBatis 的 #{})
String sql = "SELECT * FROM user WHERE name = ?";// ❌ 字符串拼接:name = "1' OR '1'='1"
String sql = "SELECT * FROM user WHERE name = '" + name + "'";
// ✅ 参数化查询(PreparedStatement / MyBatis 的 #{})
String sql = "SELECT * FROM user WHERE name = ?";MyBatis 的 #{} 与 ${}:#{} 是预编译占位符(安全),${} 是直接拼接(只在动态表名/列名且做了白名单时用)。
XSS(跨站脚本)
- 输出到 HTML 前转义(
<→<); - 富文本用白名单过滤(如 DOMPurify / OWASP Java Sanitizer);
- 设
Content-Security-Policy头。
CSRF(跨站请求伪造)
- 服务端校验 CSRF token(表单里带、服务端比对);
- 或校验
SameSiteCookie(SameSite=Lax/Strict); - 关键:GET 请求不能有副作用(否则一个
<img src="/delete/1">就能删数据)。
SSRF(服务端请求伪造)
攻击者:请服务端去请求 http://169.254.169.254/latest/meta-data/(云元数据)攻击者:请服务端去请求 http://169.254.169.254/latest/meta-data/(云元数据)防御:
- 对用户提供的 URL 做白名单(只允许预期域名);
- 解析 DNS 后校验 IP,拒绝内网地址(10.x / 172.16.x / 192.168.x / 127.x / 169.254.x);
- 禁止重定向跟随到内网。
越权(IDOR / 对象级授权)
java
// ❌ 直接用客户端传的 id,不校验归属
Order order = orderRepo.findById(request.getOrderId());
// ✅ 校验这个订单属于当前登录用户
Order order = orderRepo.findByIdAndUserId(request.getOrderId(), currentUserId());// ❌ 直接用客户端传的 id,不校验归属
Order order = orderRepo.findById(request.getOrderId());
// ✅ 校验这个订单属于当前登录用户
Order order = orderRepo.findByIdAndUserId(request.getOrderId(), currentUserId());这是实际业务里最高发的漏洞:改一下 URL 里的 id 就能看到别人的数据。每个「按 id 查询」都要带归属校验。
其他
| 攻击 | 一句话防御 |
|---|---|
| 路径穿越 | 拼接文件路径前规范化并校验在允许目录内 |
| 反序列化 | 不接受不受信任来源的序列化数据 |
| 暴力破解 | 登录限流、验证码、账户锁定 |
| 日志注入 | 用户输入入日志前转义换行 |
六、日志与脱敏
- 永远不要把密码、token、身份证、手机号完整写入日志;
- 脱敏后再记:
138****1234; - 日志里带 traceId,但别带敏感参数值。
七、防御清单(上线前过一遍)
- [ ] 所有「按 id 查询」都做了归属/权限校验
- [ ] 所有 SQL 都是参数化查询,
${}只用于白名单过的动态列名 - [ ] 密码用了 bcrypt/argon2 加盐哈希
- [ ] JWT 校验强制指定算法,payload 无敏感信息
- [ ] 输出做了 HTML 转义 / CSP 头
- [ ] 危险操作有 CSRF 防护
- [ ] 服务端发请求的地方做了 SSRF 白名单 + 内网 IP 拦截
- [ ] 密钥不在代码/仓库里
- [ ] 日志无敏感信息
八、速查
| 问题 | 答案 |
|---|---|
| 401 和 403 区别 | 未认证 vs 已认证但无权限 |
| 密码怎么存 | 加盐 + bcrypt/argon2 慢哈希 |
| JWT 最大的坑 | 不是加密、无法主动失效 |
| OAuth2 用哪种模式 | 授权码 + PKCE |
| SQL 注入怎么防 | 参数化查询,别拼字符串 |
| SSRF 怎么防 | URL 白名单 + 内网 IP 拦截 |
| 越权怎么防 | 对象级授权,按归属校验 |
Cao Note