安全 / 已完成
后端安全加固实战
理解密码 hash、token、权限校验、CORS、限流、SQL 注入、文件上传和密钥管理。
返回文章积累一句话:后端安全加固就是把常见入口守住,避免用户数据、接口权限、服务器密钥因为低级问题暴露。
本篇学完你会什么:知道上线前要检查哪些安全点,包括密码、token、权限、CORS、限流、SQL 注入、文件上传和密钥管理。
1. 安全不是最后才做的事
很多初学者会觉得:
功能先跑通,安全以后再补。这句话只对一半。功能可以先做最小版,但安全底线不能空着。
最少要先守住:
| 安全点 | 为什么重要 |
|---|---|
| 密码不存明文 | 数据库泄漏时降低伤害 |
| 接口必须校验权限 | 前端隐藏按钮不等于安全 |
| 输入必须校验 | 防止脏数据和攻击输入 |
| 密钥不能进仓库 | 避免 token、数据库被接管 |
| 上传文件要限制 | 防止恶意文件和超大文件 |
| 错误信息别暴露细节 | 不给攻击者提示 |
2. 登录和密码怎么守
密码不能明文保存
错误做法:
password = "123456"正确做法:
password_hash = hash("123456")后端只保存 hash,不保存原始密码。
常见工具:
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
return pwd_context.verify(password, password_hash)登录失败要限流
如果登录接口不限流,别人可以一直猜密码。
建议:
| 场景 | 做法 |
|---|---|
| 同一个账号连续失败 | 短时间锁定或验证码 |
| 同一个 IP 高频请求 | Redis 限流 |
| 密码太弱 | 注册/改密时拒绝 |
| 管理员账号 | 更严格的密码和登录记录 |
3. token 和权限怎么守
JWT 不是“万能通行证”。它只是证明“这个请求是谁发的”。
后端仍然要做两步:
认证 authentication:你是谁
授权 authorization:你能不能做这件事例子:
def require_permission(code: str):
async def checker(current_user = Depends(get_current_user)):
permissions = await get_user_permissions(current_user.id)
if code not in permissions:
raise HTTPException(status_code=403, detail="Forbidden")
return current_user
return checker前端隐藏按钮只是体验,真正的安全必须在后端接口上。
token 还要注意:
| 问题 | 建议 |
|---|---|
| token 过期时间太长 | access token 短一点,refresh token 单独处理 |
| token 泄漏 | HTTPS、不要写进日志 |
| 权限变化后 token 仍有效 | 服务端查权限或使用 token version |
| 退出登录 | 客户端删除 token,必要时服务端黑名单 |
4. 输入、SQL 和文件上传怎么守
输入校验
所有进入系统的数据都不应该默认相信。
Pydantic 可以先挡住很多问题:
from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=32, pattern=r"^[a-zA-Z0-9_]+$")
email: EmailStr
role_ids: list[int] = Field(default_factory=list)EmailStr 会检查邮箱格式。如果项目里还没有安装邮箱校验依赖,需要补上 email-validator。真实项目里还要确认这个邮箱是否已经被占用,以及是否需要邮箱验证流程。
校验要做在入口处,避免脏数据进入 service 和数据库。
SQL 注入
危险写法:
sql = f"SELECT * FROM users WHERE username = '{username}'"安全写法:
stmt = select(User).where(User.username == username)使用 ORM 或参数化查询,不要拼接用户输入。
文件上传
文件上传要限制:
| 限制 | 为什么 |
|---|---|
| 文件大小 | 防止占满磁盘 |
| 文件类型 | 防止上传危险脚本 |
| 文件名重命名 | 防止覆盖和路径攻击 |
| 存储位置 | 不要直接放进可执行目录 |
| 病毒/内容扫描 | 重要业务需要 |
5. CORS、限流和错误信息怎么守
CORS
开发环境可以宽一点:
http://localhost:3000
http://localhost:5173生产环境不建议直接写:
*应该明确允许真实前端域名,例如:
https://admin.example.com
https://www.example.com这里也容易误解:CORS 不是登录鉴权,它主要管“浏览器里的网页能不能读取跨域响应”。就算 CORS 配好了,后端接口仍然必须校验 token、权限和必要的 CSRF 防护。
| 场景 | 建议 |
|---|---|
| 管理后台接口 | 只允许后台真实域名 |
| 使用 Cookie 登录 | 不要用 *,并正确配置 credentials |
| 公开只读接口 | 可以更宽,但仍要考虑限流 |
| 服务端调用服务端 | CORS 不负责这类访问控制 |
限流
这些接口建议限流:
| 接口 | 原因 |
|---|---|
| 登录 | 防爆破 |
| 发送验证码 | 防短信/邮件刷量 |
| 文件上传 | 防资源耗尽 |
| AI 生成接口 | 防成本失控 |
| 搜索接口 | 防数据库压力 |
错误信息
不要把内部错误直接返回给用户:
数据库连接失败:postgresql://user:password@...对外返回:
{
"message": "服务暂时不可用,请稍后再试"
}详细堆栈放日志,不放响应体。
6. 密钥和配置怎么守
这些东西不要提交到仓库:
JWT_SECRET
DATABASE_URL
REDIS_PASSWORD
OPENAI_API_KEY
对象存储密钥
短信服务密钥推荐做法:
| 环境 | 做法 |
|---|---|
| 本地 | .env,不提交 |
| 示例 | .env.example,只放字段名和假值 |
| 测试 | CI/CD secret 或服务器环境变量 |
| 生产 | 密钥管理服务或服务器环境变量 |
还要定期检查:
git grep -n "sk-" .
git grep -n "JWT_SECRET" .
git grep -n "password=" .这不是完整扫描工具,但能先发现明显泄漏。
7. 上线前安全检查
[ ] 密码只保存 hash
[ ] 登录失败有限流
[ ] 管理接口有后端权限校验
[ ] CORS 在生产只允许真实前端域名
[ ] SQL 没有拼接用户输入
[ ] 文件上传限制大小和类型
[ ] 错误响应不暴露堆栈和密钥
[ ] token 不打印进日志
[ ] .env 不提交
[ ] 生产密钥不是默认值8. 常见错误
| 错误 | 后果 | 修正 |
|---|---|---|
| 只做前端权限 | 接口可被直接调用 | 后端每个敏感接口都校验 |
| JWT_SECRET 使用默认值 | token 可被伪造 | 使用强随机密钥 |
生产 CORS 写 * | 浏览器页面可能读取不该开放的跨域响应 | 限定真实域名,接口仍做鉴权 |
| 日志打印 token | token 泄漏 | 敏感字段脱敏 |
| 上传文件不限制 | 磁盘或安全风险 | 限大小、类型、存储目录 |
| SQL 拼字符串 | SQL 注入 | ORM 或参数化查询 |
9. 小结
| 名词 | 大白话 |
|---|---|
| 认证 | 确认你是谁 |
| 授权 | 确认你能不能做这件事 |
| hash | 密码的不可逆保存结果 |
| CORS | 浏览器跨域访问规则 |
| 限流 | 控制接口访问频率 |
| 密钥 | 系统的钥匙,不该进仓库 |
上一篇建议:先看《CI/CD、发布和回滚》。
下一篇建议:继续看《数据库备份、迁移和恢复》,补上数据安全。