个人博客安全加固实践:从 SHA256 到 bcrypt,从 admin_password 到 JWT 统一认证

2026年6月24日 blogTech 6 分钟阅读 4 次阅读
📖 文章摘要

个人博客 blog.vue2.xyz 的系统级安全升级记录——密码哈希从 SHA256 升级到 bcrypt、统一 JWT 认证、XSS 消毒、路径穿越防护等全链路加固实践。

背景

个人博客 blog.vue2.xyz 经过近一年的迭代,功能日益完善。随着网盘、图床、后台管理等功能上线,认证体系和安全防护逐渐成为需要认真对待的问题。本文记录了 2026 年 6 月的一次安全大升级——从密码哈希到 XSS 消毒、从路径穿越到统一认证,系统性地加固了整个项目。

一、密码哈希:从无盐 SHA256 到 bcrypt

旧方案的问题

原先的密码存储非常简单,就是裸 SHA256:

def hash_password(password: str) -> str:
    return hashlib.sha256(password.encode()).hexdigest()

没有盐、没有迭代拉伸、没有常量时间比较。数据库泄露后,密码可被彩虹表或 GPU 暴力破解快速还原。

升级到 bcrypt

引入了 passlib[bcrypt],密码哈希改为 bcrypt,同时保留旧哈希兼容:

from passlib.hash import bcrypt

def hash_password(password: str) -> str:
    return bcrypt.hash(password)

def verify_password(plain: str, hashed: str) -> bool:
    if hashed.startswith("$2b$"):
        return bcrypt.verify(plain, hashed)
    return hashlib.sha256(plain.encode()).hexdigest() == hashed

自动迁移策略

旧版 SHA256 哈希在用户下次登录时自动升级为 bcrypt,无缝过渡:

if is_legacy_hash(admin.password_hash):
    admin.password_hash = hash_password(data.password)
    db.commit()

用户无需任何操作,旧密码可以一直用到下次登录,登录瞬间自动完成升级。

二、统一 JWT 认证

旧方案的问题

升级前,不同功能用了不同的认证方式:后台管理走 JWT,图床上传通过 admin_password FormData 字段,网盘上传通过 admin_password URL 查询参数。网盘上传尤其危险——管理员密码通过 URL 参数传递,Nginx 的 access log 和浏览器历史都会留下明文密码。

统一为 JWT

所有上传接口统一改为 Depends(get_current_admin):

@router.post("/upload")
async def upload_pan(
    ...,
    admin: Admin = Depends(get_current_admin),
):
    ...  # 不再需要 admin_password 参数

前端同步更新:每次上传前手动输入密码 → 调用 /api/auth/login 获取 JWT → 带 Authorization: Bearer 头上传。

const token = useCookie("blog_token", {
  maxAge: 60 * 60 * 24 * 7,
  sameSite: "lax",
  path: "/",
  secure: import.meta.dev !== true,
})

三、XSS 消毒

旧方案的问题

文章内容使用 marked 库将 Markdown 转 HTML,然后通过 Vue 的 v-html 直接渲染。marked v14 默认允许原始 HTML,Markdown 中的 script 标签会被直接执行。同样的问题出现在 About 页面、后台预览弹窗等 7 个渲染点。

引入 DOMPurify

选择了 isomorphic-dompurify(SSR 兼容版本),在所有 v-html 渲染前消毒:

import DOMPurify from "isomorphic-dompurify"

const html = computed(() => {
  return DOMPurify.sanitize(marked(props.content, { breaks: true }))
})

覆盖 ContentRenderer.vue、文章详情页、关于页、编辑器预览、后台预览弹窗等全部渲染点。

四、路径穿越防护

文件伺服

原始代码直接用用户传入的文件名拼接路径,攻击者可构造 /uploads/../data/blog.db 直接下载整个 SQLite 数据库。

修复

filepath = (UPLOAD_DIR / filename).resolve()
if not str(filepath).startswith(str(UPLOAD_DIR.resolve())):
    raise HTTPException(status_code=403, detail="Forbidden")

后台上传文件的 delete/list/cleanup 接口也统一修复。

五、网盘文件类型白名单

网盘上传扩展名黑名单从 11 个扩充到 22 个,新增 .php/.asp/.jsp/.jar/.dll/.wasm 等危险后缀,同时修复了 filename=None 时绕过检测的漏洞。

六、其他加固

SECRET_KEY 运行时检查

生产环境忘记设置 SECRET_KEY 时,后端启动直接报错,防止默认密钥被利用。

CORS 白名单

从 ["*"] 收紧为 [SITE_URL, "http://localhost:3000"],由环境变量控制。

管理员用户名保护

admin-username 端点加上 JWT 认证,防止匿名枚举。

登录支持无用户名

单用户博客场景,登录接口支持不传 username,后端自动匹配唯一管理员。

七、总结

这次安全升级覆盖了从密码存储到前端渲染、从 API 认证到文件访问的完整链路:密码存储从 SHA256 升级到 bcrypt 并支持自动迁移,API 认证统一为 JWT,前端 XSS 通过 DOMPurify 全面消毒,文件访问添加路径穿越防护,配置安全通过运行时检查兜底,CORS 从通配符收紧为白名单。

全部代码已开源于 GitHub。

最后更新:2026年6月29日CC BY-NC-SA 4.0

评论

暂无评论,来写第一条吧

© 2026 My Blog. Built with Nuxt.js + FastAPI.