Flask 博客后台权限最小化:管理员/作者分离、装饰器校验与 Nginx 二次防护
很多个人博客最早只有一个后台账号,功能越加越多后,问题也会一起长出来:投稿用户能不能误进管理页,运营同学要不要拿最高权限,登录页被扫时除了应用层还能不能再挡一层。对 Flask 项目来说,后台权限不应该只靠“登录了就行”,而应该先把身份边界拆清楚,再把路由、数据库和反向代理各自负责的事情做好。
这篇文章给一套适合个人博客和小团队内容站的最小权限方案,核心目标只有三个:后台路由默认拒绝、账号权限尽量拆小、即使应用层漏了一处判断,Nginx 也能再拦一次。
1. 先把权限模型定简单
如果你的博客和当前这类项目一样,前台读者账号、作者账号、管理员账号职责不同,最稳妥的做法不是所有人共用一张表再靠布尔字段硬分,而是至少明确出两类能力:
author:写稿、改自己的文章、查看自己的草稿。admin:审核、发布、分类标签维护、站点配置、评论治理。
小站点有两种落地方式:
- 像现有项目一样把后台管理员和站点用户拆成两张表。优点是边界直接,前台登录态和后台登录态天然分离。
- 用一张用户表加
role字段,后续再扩展editor、reviewer。优点是结构统一,迁移到 MySQL 时也更容易做报表和审计。
如果你未来打算从 SQLite 迁到 MySQL,建议一开始就把 role、is_active、last_login_at 这些字段设计出来,避免后面为了权限补字段时顺便改业务逻辑。
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(128), nullable=False)
role = db.Column(db.String(20), nullable=False, default='author', index=True)
is_active = db.Column(db.Boolean, nullable=False, default=True)
last_login_at = db.Column(db.DateTime)
2. 路由保护不要只停在 login_required
很多 Flask 项目会在后台页面统一加 @login_required,这只能保证“用户已登录”,不能保证“用户有资格进入这里”。最常见的坑是作者账号也能打开 /admin,只是页面里某些按钮再做限制,这种做法等于把授权逻辑推迟到了模板层。
更稳的方式是把权限判断前移到视图装饰器里:
from functools import wraps
from flask import abort
from flask_login import current_user, login_required
def roles_required(*roles):
def decorator(view):
@wraps(view)
@login_required
def wrapped(*args, **kwargs):
if not getattr(current_user, 'is_active', True):
abort(403)
if getattr(current_user, 'role', None) not in roles:
abort(403)
return view(*args, **kwargs)
return wrapped
return decorator
@app.route('/admin/articles')
@roles_required('admin', 'editor')
def admin_articles():
...
这段代码有两个关键点:
- 用
@wraps保留原函数信息,避免路由和调试信息混乱。 - 先登录校验,再做角色判断,未登录返回跳转,已登录但越权直接
403。
如果你的项目像当前仓库一样已经有 admin_required,那就继续往前走一步,把单一的管理员判断升级成可复用的角色装饰器,后面新增“审核员”时不需要重新复制一套视图函数。
3. 文章归属和数据过滤必须在查询层完成
权限控制不能只写在“按钮是否可见”。作者看不到“编辑别人文章”的按钮,不代表他不能手工拼 URL。真正有效的做法是把数据过滤写进查询条件里。
@app.route('/dashboard/my-articles')
@roles_required('author', 'admin')
def my_articles():
query = Article.query
if current_user.role == 'author':
query = query.filter(Article.author_user_id == current_user.id)
articles = query.order_by(Article.created_at.desc()).all()
return render_template('my_articles.html', articles=articles)
管理员可以看全量,作者只能看自己的数据。这个规则同样适用于评论审核、媒体库、自动化发布记录和 SEO 配置页面。只要对象有归属关系,就把“当前用户是否拥有这条记录”的判断写进 SQLAlchemy 查询,而不是靠前端隐藏。
4. SQLite 能跑,MySQL 更适合审计和并发
个人博客前期用 SQLite 没问题,权限方案也完全可以先在 SQLite 上落地。但一旦后台开始有多人协作,或者你要记录更多登录审计、操作日志、发布记录,MySQL 会更省心。
迁移时重点关注三件事:
- 给
username、role、status、author_user_id建索引,后台列表页和审计页查询会稳定很多。 - 把发布时间、登录时间、封禁时间统一用 UTC 存储,避免服务器换时区后审计记录混乱。
- 不要把密码明文、API token 明文写进数据库备份包,备份文件的访问权限要单独收紧。
如果暂时还在 SQLite,至少把数据库文件放在 Web 根目录之外,并且把备份目录排除出静态文件映射。
5. 给 Nginx 一层“后台门卫”
应用层授权做得再认真,也值得给 /admin 再加一层 Nginx 防护。原因很现实:装饰器可能漏挂、蓝图可能新开路由、临时调试页可能忘记删除。反向代理层的白名单或基础认证,能把很多扫描流量挡在 Flask 之前。
一个实用配置是把固定办公 IP 放进白名单,同时给非白名单流量启用 Basic Auth:
location /admin/ {
proxy_pass http://127.0.0.1:5000;
satisfy any;
allow 127.0.0.1;
allow 203.0.113.10;
deny all;
auth_basic 'Restricted Admin';
auth_basic_user_file /etc/nginx/.htpasswd-blog-admin;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
这里的含义是:白名单 IP 可以直接访问,其他来源即使能连到站点,也要再过一次用户名密码。对于只有自己维护的个人博客,这一层通常比“把后台改成冷门路径”有效得多。
6. 上线时顺手补三项配套措施
权限做完后,建议把这三项一起补上:
- 管理员密码重置走命令行或受控后台,不要把默认密码留在部署脚本里。
- 登录失败做节流或锁定,至少限制暴力尝试频率。
- 记录
last_login_at、登录失败次数和关键后台操作,方便后面排查“谁改了配置、谁发布了文章”。
如果你的博客已经接了自动化发布脚本,还要把发布 token 和管理员账号分开。发布 API token 只允许调用发布接口,不要让它等价于后台超级管理员密码。
7. 最容易踩的坑
- 只有模板判断,没有视图判断。结果是按钮藏住了,URL 仍然能访问。
- 只校验角色,不校验记录归属。作者可能改到别人的文章。
- 在开发环境用 SQLite 没问题,生产环境却把数据库文件放进可下载目录。
- 以为 Nginx Basic Auth 能替代应用层授权。它只能加门槛,不能代替业务权限模型。
- 自动化脚本直接复用管理员凭据。一旦泄漏,风险范围会被放大。
8. 我的建议落地顺序
如果你现在就想改一版,按这个顺序最稳:先给后台路由补装饰器,再把文章和评论列表改成按归属过滤,然后补 last_login_at 和登录失败节流,最后在 Nginx 上给 /admin 增加白名单加基础认证。这样做的好处是每一步都能独立上线,不需要一次性重构整套账号系统。
总结
Flask 博客后台安全的重点不在“技术栈高级”,而在边界有没有收紧。登录、角色、数据归属、反向代理,这四层只要每层都多做一点,个人站点就能比“只有一个后台密码”的状态稳很多。对小团队博客来说,最小权限不是复杂化,而是把本来迟早要补的授权规则,提前用能维护的方式落下来。