Web安全实战指南:从加密到认证授权
前言:安全是后端的底线
安全问题往往不出事则以,一出事就是大事。数据泄露、账户被盗、服务被攻击——每一个都可能让产品直接下线。
Web 安全的核心问题:
- 机密性:数据不被未授权访问
- 完整性:数据不被篡改
- 可用性:服务不被破坏
- 身份认证:确认"你是谁"
- 授权:确认"你能干什么"
一、加密技术
1.1 对称加密 vs 非对称加密
| 类型 | 特点 | 算法 |
|---|---|---|
| 对称加密 | 加解密同一密钥,速度快 | AES、DES(已不安全) |
| 非对称加密 | 公钥加密,私钥解密 | RSA、ECC |
| 哈希 | 单向不可逆 | SHA-256、MD5(已不安全) |
| 密码哈希 | 专为密码设计,慢 | bcrypt、Argon2 |
1.2 AES 加密的坑
坑一:ECB 模式不安全
# 错误:ECB 模式
cipher = AES.new(key, AES.MODE_ECB)
# 相同明文块 → 相同密文块,泄露模式信息
坑二:用 MD5 存密码
# 错误:MD5 已不安全
import hashlib
hashlib.md5(password.encode()).hexdigest()
1.3 GCM 模式(推荐)
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
def encrypt_gcm(data, key):
nonce = get_random_bytes(12) # GCM 推荐 12 字节
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(data)
return nonce + tag + ciphertext # 拼接返回
def decrypt_gcm(encrypted_data, key):
nonce = encrypted_data[:12]
tag = encrypted_data[12:28]
ciphertext = encrypted_data[28:]
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
return cipher.decrypt_and_verify(ciphertext, tag) # 同时验证完整性
GCM 优势:
- 认证加密,同时保证机密性和完整性
- 不需要额外填充
- 性能好(硬件加速)
1.4 密码哈希
import bcrypt
def hash_password(password):
salt = bcrypt.gensalt(rounds=12) # rounds 越大越慢越安全
return bcrypt.hashpw(password.encode(), salt)
def verify_password(password, hashed):
return bcrypt.checkpw(password.encode(), hashed)
推荐算法:
- Argon2:目前最强(Password Hashing Competition 冠军)
- bcrypt:成熟稳定
- PBKDF2:NIST 标准
1.5 密钥管理
# 错误:硬编码
ENCRYPTION_KEY = b'this-is-a-32-byte-key'
# 推荐:密钥管理服务
import boto3
kms = boto3.client('kms')
def generate_data_key():
response = kms.generate_data_key(
KeyId='alias/my-key',
KeySpec='AES_256'
)
return response['Plaintext'], response['CiphertextBlob']
信封加密模式:
- 用 KMS 生成数据密钥(明文 + 密文)
- 用明文数据密钥加密数据
- 存储加密后的数据 + 密文数据密钥
- 用时用 KMS 解密数据密钥,再解密数据
二、身份认证
2.1 Session 认证
from flask import Flask, session
app = Flask(__name__)
app.secret_key = 'your-secret-key'
@app.route('/login', methods=['POST'])
def login():
user = authenticate_user(username, password)
if user:
session['user_id'] = user.id
return {'message': 'Logged in'}
return {'error': 'Invalid credentials'}, 401
@app.route('/profile')
@login_required
def profile():
return {'user_id': session['user_id']}
Session 存储选择:
- 内存:开发用
- Redis:生产推荐
- 数据库:持久化需求
2.2 JWT 认证
const jwt = require('jsonwebtoken');
function generateToken(user) {
const payload = {
userId: user.id,
roles: user.roles,
};
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '15m', // 短期 token
issuer: 'your-domain.com',
audience: 'your-api'
});
}
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (err) {
if (err.name === 'TokenExpiredError') throw new Error('Token expired');
throw new Error('Invalid token');
}
}
2.3 JWT 的坑
坑一:payload 太大
把所有用户资料塞进去,token 长到 4KB。只放必要信息:用户 ID、角色。
坑二:过期时间太长
设 24 小时 → 用户登出后 token 仍有效。用 15 分钟短期 token + refresh token。
坑三:签名算法用错
// 危险:允许 algorithm: none
jwt.verify(token, secret, { algorithms: ['HS256', 'none'] });
// 正确:明确指定算法
jwt.verify(token, secret, { algorithms: ['HS256'] });
2.4 Token 刷新机制
async function refreshAccessToken(refreshToken) {
// refresh token 通常存数据库,登出时删除
const user = await verifyRefreshToken(refreshToken);
return generateToken(user);
}
双 Token 机制:
- Access Token:15 分钟过期,用于 API 访问
- Refresh Token:7-30 天过期,存数据库,用于刷新
2.5 Session vs JWT
| 维度 | Session | JWT |
|---|---|---|
| 状态 | 有状态(服务端存) | 无状态 |
| 扩展性 | 需要共享 session | 天然支持分布式 |
| 注销 | 简单(删除 session) | 复杂(需要黑名单) |
| 大小 | cookie 短 | token 长 |
| 适用 | 单体应用 | 微服务、API |
三、OAuth 2.0
3.1 四种授权模式
| 模式 | 场景 |
|---|---|
| 授权码模式 | Web 应用(最常用、最安全) |
| 简化模式 | 单页应用 SPA |
| 密码模式 | 自己的应用(高信任) |
| 客户端凭证 | 服务间调用 |
3.2 授权码模式流程
3.3 OAuth 2.0 实现
// 授权码模式示例
const authUrl = `https://auth.example.com/authorize?` +
`response_type=code&` +
`client_id=YOUR_CLIENT_ID&` +
`redirect_uri=https://yourapp.com/callback&` +
`scope=read write&` +
`state=RANDOM_STATE`;
// 回调处理
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// 验证 state 防 CSRF
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
// 用 code 换 token
const tokenResponse = await axios.post('https://auth.example.com/token', {
grant_type: 'authorization_code',
code,
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
redirect_uri: 'https://yourapp.com/callback'
});
const { access_token, refresh_token } = tokenResponse.data;
// 存储 token,完成登录
});
四、授权(Authorization)
4.1 角色授权
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user.roles || !req.user.roles.some(r => roles.includes(r))) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.delete('/api/users/:id',
authMiddleware,
requireRole('admin'),
deleteUser
);
4.2 权限授权(更细粒度)
function requirePermission(permission) {
return async (req, res, next) => {
const userPermissions = await getUserPermissions(req.user.id);
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.put('/api/posts/:id',
authMiddleware,
requirePermission('post:edit'),
updatePost
);
4.3 资源级授权
async function requireOwnership(req, res, next) {
const postId = req.params.id;
const post = await Post.findById(postId);
if (!post) return res.status(404).json({ error: 'Not found' });
if (post.authorId !== req.user.id && !req.user.roles.includes('admin')) {
return res.status(403).json({ error: 'Forbidden' });
}
req.post = post; // 后面直接用
next();
}
app.delete('/api/posts/:id',
authMiddleware,
requireOwnership,
deletePost
);
4.4 RBAC vs ABAC
| 模型 | 说明 | 适用 |
|---|---|---|
| RBAC | 基于角色 | 大部分场景 |
| ABAC | 基于属性(用户/资源/环境) | 复杂权限需求 |
| ACL | 访问控制列表 | 简单系统 |
五、XSS 防御
5.1 XSS 的三种类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 存储型 | 恶意脚本存到数据库 | 评论里塞 <script> |
| 反射型 | 从 URL 参数反射 | ?q=<script> |
| DOM 型 | 前端代码漏洞 | innerHTML = userInput |
5.2 防御措施
输出编码:
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// React 默认转义
<div>{userInput}</div> // 安全
// dangerouslySetInnerHTML 才会执行 HTML
<div dangerouslySetInnerHTML={{__html: userInput}} /> // 危险!
CSP(Content Security Policy):
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
HttpOnly Cookie:
res.cookie('token', token, {
httpOnly: true, // JS 不能访问
secure: true, // 只在 HTTPS 传输
sameSite: 'strict' // CSRF 防护
});
六、CSRF 防御
6.1 CSRF 原理
用户登录 A 网站后,访问恶意网站 B,B 伪造请求冒充用户向 A 发起操作。
6.2 防御措施
Anti-CSRF Token:
// 生成 token
app.get('/form', (req, res) => {
req.csrfToken = generateToken();
res.render('form', { csrfToken: req.csrfToken });
});
// 验证 token
app.post('/form', (req, res) => {
if (req.body.csrfToken !== req.csrfToken) {
return res.status(403).send('CSRF token invalid');
}
// 处理表单
});
SameSite Cookie:
res.cookie('session', sessionId, {
sameSite: 'strict' // 或 'lax'
});
校验 Referer:
app.use((req, res, next) => {
const referer = req.headers.referer;
if (req.method === 'POST' && !referer.startsWith('https://yourdomain.com')) {
return res.status(403).send('Invalid referer');
}
next();
});
七、SQL 注入防御
7.1 参数化查询(最有效)
# 错误:字符串拼接(SQL 注入!)
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# 正确:参数化查询
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
// Node.js
connection.query('SELECT * FROM users WHERE name = ?', [name]);
7.2 ORM 的好处
# Django ORM 自动参数化
User.objects.filter(name=name)
# SQLAlchemy
session.query(User).filter(User.name == name)
八、API 安全
8.1 限流(Rate Limiting)
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 请求
message: 'Too many requests'
});
app.use('/api/', limiter);
8.2 CORS 配置
const cors = require('cors');
app.use(cors({
origin: ['https://yourdomain.com'], // 不要用 '*'
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
8.3 Helmet 加固 HTTP 头
const helmet = require('helmet');
app.use(helmet()); // 加一堆安全头
8.4 输入验证
const Joi = require('joi');
const schema = Joi.object({
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150),
name: Joi.string().alphanum().min(3).max(30).required()
});
app.post('/api/users', (req, res) => {
const { error, value } = schema.validate(req.body);
if (error) return res.status(400).json({ error: error.details[0].message });
// 处理 value
});
九、HTTPS 与传输安全
9.1 强制 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
add_header Strict-Transport-Security "max-age=31536000" always;
}
9.2 证书管理
# Let's Encrypt 免费证书
certbot certonly --webroot -w /var/www/html -d example.com
# 自动续期
crontab -e
0 12 * * * /usr/bin/certbot renew --quiet
十、安全审计与监控
10.1 日志记录
import logging
security_logger = logging.getLogger('security')
def log_security_event(event_type, user_id, details):
security_logger.warning({
'event': event_type,
'user_id': user_id,
'ip': request.remote_addr,
'user_agent': request.headers.get('User-Agent'),
'timestamp': datetime.utcnow().isoformat(),
'details': details
})
# 关键事件
log_security_event('login_success', user.id, {})
log_security_event('login_failed', None, {'username': username})
log_security_event('permission_denied', user.id, {'resource': '/admin'})
log_security_event('suspicious_activity', user.id, {'reason': 'multiple_failed_attempts'})
10.2 异常检测
def detect_brute_force(username, ip):
"""检测暴力破解"""
recent_failures = get_recent_failures(username, ip, minutes=15)
if len(recent_failures) >= 5:
lock_account(username)
notify_security_team(username, ip)
十一、常见安全漏洞清单(OWASP Top 10)
| 漏洞 | 防御 |
|---|---|
| 注入 | 参数化查询 |
| 失效认证 | 强密码策略、MFA |
| 敏感数据泄露 | 加密存储、HTTPS |
| XML 外部实体 | 禁用 DTD |
| 失效访问控制 | 服务端校验权限 |
| 安全配置错误 | 默认配置审查 |
| XSS | 输出编码、CSP |
| 不安全反序列化 | 输入校验 |
| 已知漏洞组件 | 依赖扫描 |
| 日志监控不足 | 完善日志 |
十二、安全踩坑总结
坑一:信任客户端
// 错误:前端校验就够了
// 实际上:前端校验只是体验,后端必须再校验
// 错误:隐藏菜单就是权限控制
// 实际上:每个 API 必须服务端校验权限
坑二:密钥硬编码
代码泄露 = 密钥泄露。用密钥管理服务。
坑三:日志记录敏感信息
# 错误
logger.info(f"User login: {username}, password: {password}")
# 正确
logger.info(f"User login attempt: {username}")
坑四:错误信息泄露
# 错误:把异常详情返回给用户
return {'error': str(e), 'stack': traceback.format_exc()}
# 正确:返回通用错误,详情记日志
logger.error(f"Error: {e}", exc_info=True)
return {'error': 'Internal server error'}
坑五:忘记 HTTPS
HTTP 传输密码等于明文传输。生产环境必须 HTTPS。
坑六:CORS 配置成 *
// 错误
app.use(cors({ origin: '*' }));
// 正确:明确指定域名
app.use(cors({ origin: ['https://yourdomain.com'] }));
十三、安全检查清单
认证
- 密码用 bcrypt/Argon2 哈希
- JWT 过期时间合理(15 分钟)
- 支持 token 刷新
- 登录失败有限流
- 支持 MFA(敏感操作)
授权
- 每个 API 都有权限校验
- 资源级权限检查
- 服务端校验,不依赖前端
数据
- 敏感数据加密存储
- 密钥用 KMS 管理
- 不日志敏感信息
- 数据库连接加密
传输
- 强制 HTTPS
- HSTS 头
- 证书自动续期
防护
- XSS 防御(CSP、输出编码)
- CSRF 防御(SameSite、Token)
- SQL 注入防御(参数化查询)
- 限流
- Helmet 安全头
监控
- 安全事件日志
- 异常检测(暴力破解等)
- 告警机制
十四、写在最后
安全不是一次性工作,是持续的过程。
几条核心原则:
- 永远不信任客户端输入
- 默认拒绝,显式允许
- 纵深防御:多层安全保障
- 最小权限原则:只给必需的权限
- 安全左移:设计阶段就考虑安全
- 定期审计:代码审查 + 渗透测试
- 保持更新:依赖、补丁、证书
安全工程没有"做完"的那天。 攻击手法在变,防御手段也要跟着演进。但底层原则不变:不信任、最小化、可观测、可恢复。
做安全最大的敌人不是黑客,是侥幸心理。“应该不会有人攻击我们吧”——这句话往往是出事前的最后一念。
本文整合了 8 篇 Web 安全相关文章,涵盖加密技术、身份认证(Session/JWT/OAuth2)、授权(RBAC/ABAC)、XSS/CSRF 防御、SQL 注入、API 安全、HTTPS、安全审计等核心技术。
版权声明: 本文首发于 指尖魔法屋-Web安全实战指南:从加密到认证授权(https://blog.thinkmoon.cn/post/web-security-comprehensive-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。