Django 实战:做一个个人博客 · 第 7 / 8 篇(展开目录)

全系列用 Django 5.2 LTS 做一个个人博客:游客看文章,作者登录后写文章,管理员维护分类和账号。Django1—Django8 是文章序号,不是 Django 版本号。

  1. Django1:起步——安装、运行与项目结构
  2. Django2:配置与路由——一个请求在 Django 里怎么走
  3. Django3:模型与迁移——把博客的数据存进数据库
  4. Django4:ORM——查询、更新、统计与事务
  5. Django5:页面——视图、模板、搜索与分页
  6. Django6:表单与后台——让作者在网页上写文章
  7. Django7:登录与权限——谁能改哪篇文章
  8. Django8:上线与维护——测试、部署、备份与发布

下载可运行的示例项目(ZIP)

约 20 分钟

本篇目标: 实现登录和退出;保证作者只能修改自己的文章。

核心一句话: “已经登录”和“有权修改这篇文章”是两个判断,缺一不可。

1. 先把权限规则写成表

操作 游客 普通作者 管理员
看公开文章 ✅ ✅ ✅
通过公开地址看草稿 ❌ ❌ ❌
在网站上写文章 先登录 ✅ ✅
编辑 / 删除自己的文章 — ✅ ✅
编辑 / 删除别人的文章 ❌ ❌ 网站上 ❌,后台 ✅
进入 Admin 后台 ❌ ❌ ✅

本例不开放注册,账号由管理员创建。管理员在网站前台也只能改自己的文章,跨作者维护统一到 Admin 里做。

2. 登录后,服务器怎么认出你

提交用户名和密码
  → LoginView 用认证后端核对密码哈希
  → 成功后在服务器端创建会话,把会话 ID 放进 Cookie 发给浏览器
  → 之后每次请求,浏览器自动带上这个 Cookie
  → SessionMiddleware 找到会话,AuthenticationMiddleware 设置 request.user

Cookie 里只有一个随机的会话 ID,没有密码。密码在数据库里也只存哈希值——永远不要自己建一个明文密码字段。

3. 配置登录和退出

config/urls.py 里加入 Django 自带的两个视图(保留原有的 admin 和 blog 路由):

from django.contrib.auth import views as auth_views
from django.urls import path

# 以下条目加入原有 urlpatterns,保留 admin 和 blog 的路由。
urlpatterns = [
    path("accounts/login/", auth_views.LoginView.as_view(), name="login"),
    path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
]

settings.py:

LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "blog:mine"
LOGOUT_REDIRECT_URL = "blog:list"

未登录访问受保护页面时,会跳到 /accounts/login/?next=原来的地址;登录后自动回到原来的地址。内置的 LoginView 会检查 next 是不是本站地址,防止被利用跳到钓鱼网站。

4. 登录模板和退出按钮

templates/registration/login.html(这是 LoginView 默认查找的路径):

{% extends 'base.html' %}
{% block content %}
<h1>登录</h1>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="hidden" name="next" value="{{ next }}">
  <button type="submit">登录</button>
</form>
{% endblock %}

退出按钮在 base.html 里,用的是 POST 表单:

<form method="post" action="{% url 'logout' %}">
  {% csrf_token %}
  <button type="submit">退出</button>
</form>

退出不能用普通链接

Django 5 的 LogoutView 只接受 POST。写成 <a href="/accounts/logout/"> 会得到 405 错误。这样设计是为了防止别的网站放一张图片就把你“退出”了。

5. 创建普通作者账号

最简单的方法:用管理员登录 /admin/ → 用户 → 新增 Alice 和 Bob。不要勾选“工作人员状态”和“超级用户状态”。

也可以在 shell 里创建:

from getpass import getpass
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password

User = get_user_model()
user = User(username="alice")
password = getpass("设置 Alice 的密码:")
validate_password(password, user=user)
user.set_password(password)
user.save()

set_password() 负责把密码变成哈希。注意 create_user() 本身不会检查密码强度,所以这里显式调用了 validate_password()。

6. 两道检查:登录了吗?是你的吗?

编辑视图的核心(完整代码见第 6 篇):

@login_required
@require_http_methods(["GET", "POST"])
def post_update(request, pk):
    post = get_object_or_404(Post, pk=pk, author=request.user)
    # 接着绑定并处理 PostForm,完整实现见第六篇。
检查 回答的问题 失败时
@login_required 登录了吗? 302 跳转到登录页
查询条件 author=request.user 这篇文章是你的吗? 404

选择不同的人去编辑 Alice 的文章,看看两道检查分别在哪里拦住了谁:

交互演示加载中…(需要启用 JavaScript)

为什么返回 404 而不是 403?404 让无权的人分不清“文章不存在”和“文章是别人的”,少泄露一点信息。用 403 也可以,但要整个网站保持一致,并且 GET 和 POST 都遵守同一规则。

新建文章时,作者由服务器设置(post.author = request.user),表单里没有作者字段。即使 Bob 伪造一个 author=Alice 的字段提交,作者仍然是 Bob。

7. User、Group 与权限

概念 用途 本例怎么用
User 账号与身份 文章的作者
Group 给一组人统一配置权限 可以建一个“编辑组”
is_staff 能否进入 Admin 普通作者不设置
is_superuser 拥有全部权限 只给管理员
模型权限(如 blog.change_post) 能否操作某一类数据 Admin 根据它决定能做什么
对象权限 能否操作某一条数据 前台用 author=request.user 实现

两个容易误解的点:

  • is_staff 只是能进后台大门,不代表拥有所有模型权限。
  • login_required 不检查模型权限。如果希望只有“编辑组”才能写文章,加上 @permission_required("blog.add_post", raise_exception=True),再把权限分给编辑组。即便如此,编辑和删除仍然需要归属判断——Django 默认的权限系统不知道“这篇文章是谁的”。

8. 要不要自定义用户模型

本系列用 Django 默认的用户模型,最简单。真实的新项目如果需要邮箱登录、手机号等字段,在第一次 migrate 之前就定义一个继承 AbstractUser 的用户模型,并设置 AUTH_USER_MODEL。

项目已经有数据之后再换用户模型非常麻烦,不是改一行配置就能完成的。无论用哪种,代码里引用用户都写 settings.AUTH_USER_MODEL(模型字段)或 get_user_model()(运行时)。

9. 上线后的登录安全

  • 启用 HTTPS 后,示例会同时打开 SESSION_COOKIE_SECURE 和 CSRF_COOKIE_SECURE。此时如果还用 HTTP 访问,浏览器不发送这些 Cookie,表现为“怎么都登录不上”或 CSRF 失败。
  • CSRF 防的是“别的网站借你的浏览器身份提交表单”,它不防暴力猜密码。公网开放的登录入口还需要限流、失败监控和找回密码流程——Django 自带的登录视图没有内置登录失败限速。

验收

用三个浏览器窗口(或无痕窗口)分别当游客、Alice 和 Bob:

  • 游客访问 /posts/new/ → 跳到登录页,登录后回到写文章页
  • Bob 直接访问 Alice 文章的编辑和删除地址 → 404
  • 用普通链接访问 /accounts/logout/ → 405;点击退出按钮 → 正常退出

下一篇会把这些检查写成自动化测试,以后每次改代码都能一键验证。

小练习

1. 只保留 @login_required,去掉查询里的 author=request.user,会出现什么漏洞?

任何登录用户都能通过改 URL 里的数字,编辑或删除任何人的文章。这类漏洞叫“越权访问”(IDOR),是 Web 应用里最常见的安全问题之一。

2. 想让 Alice 能在网站前台编辑 Bob 的文章(比如她是主编),怎么改最合理?

给“主编”建一个 Group,赋予 blog.change_post 权限;在编辑视图里判断:如果 request.user.has_perm("blog.change_post"),查询时不限定作者,否则仍加 author=request.user。删除视图同理。