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']

信封加密模式:

  1. 用 KMS 生成数据密钥(明文 + 密文)
  2. 用明文数据密钥加密数据
  3. 存储加密后的数据 + 密文数据密钥
  4. 用时用 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

维度SessionJWT
状态有状态(服务端存)无状态
扩展性需要共享 session天然支持分布式
注销简单(删除 session)复杂(需要黑名单)
大小cookie 短token 长
适用单体应用微服务、API

三、OAuth 2.0

3.1 四种授权模式

模式场景
授权码模式Web 应用(最常用、最安全)
简化模式单页应用 SPA
密码模式自己的应用(高信任)
客户端凭证服务间调用

3.2 授权码模式流程

sequenceDiagram participant U as 用户 participant C as 客户端 participant A as 授权服务器 participant R as 资源服务器 U->>C: 点击登录 C->>A: 重定向到授权页 U->>A: 同意授权 A->>C: 返回授权码 code C->>A: 用 code 换 token A->>C: 返回 access_token C->>R: 用 token 访问资源 R->>C: 返回数据

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 安全头

监控

  • 安全事件日志
  • 异常检测(暴力破解等)
  • 告警机制

十四、写在最后

安全不是一次性工作,是持续的过程。

几条核心原则:

  1. 永远不信任客户端输入
  2. 默认拒绝,显式允许
  3. 纵深防御:多层安全保障
  4. 最小权限原则:只给必需的权限
  5. 安全左移:设计阶段就考虑安全
  6. 定期审计:代码审查 + 渗透测试
  7. 保持更新:依赖、补丁、证书

安全工程没有"做完"的那天。 攻击手法在变,防御手段也要跟着演进。但底层原则不变:不信任、最小化、可观测、可恢复。

做安全最大的敌人不是黑客,是侥幸心理。“应该不会有人攻击我们吧”——这句话往往是出事前的最后一念。


本文整合了 8 篇 Web 安全相关文章,涵盖加密技术、身份认证(Session/JWT/OAuth2)、授权(RBAC/ABAC)、XSS/CSRF 防御、SQL 注入、API 安全、HTTPS、安全审计等核心技术。

版权声明: 本文首发于 指尖魔法屋-Web安全实战指南:从加密到认证授权https://blog.thinkmoon.cn/post/web-security-comprehensive-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!