从"能看"到"能用"差一个登录
第8章的博客虽然能跑,但有一个明显的硬伤:任何人打开浏览器访问 /article/ 都能看到文章列表。一个真正能用的博客系统,得先校验"你是谁、有没有权限"——这就是登录态。
这节课我们用 Django 内置的 Session 会话机制,把"登录态"加到第8章的博客上。结束时博客就会变成:
- 没登录访问
/article/→ 自动跳到登录页 - 输入正确账号密码 → 跳回文章列表,能看了
- 点"退出登录" → 又跳回登录页,再也看不到列表
/article/ 能看到文章列表)。如果重启了电脑或换了机器,先在 blog 项目目录下用 python3 manage.py runserver 0.0.0.0:8000 把服务启起来,再继续。
为什么需要 Session?
所以登录这件事必须靠"额外手段":用户提交正确的账号密码后,服务器在自己这边记一笔"这家伙登过了",下次他再来就放行。这一笔记录就叫 Session。
HTTP 的"金鱼记忆"
想象你去食堂打饭:
- 第一次:你跟阿姨说"我要一份土豆丝",阿姨打给你。
- 第二次:你又来了,问"我刚才点的什么?"——阿姨完全不记得你,更不记得你点过啥。
HTTP 就是这样。每个请求都是独立的,服务器处理完就忘。所以光靠 HTTP 本身,登录就是个不可能完成的任务——服务器记不住"上一个请求是你登的"。
Session 是怎么补上这个洞的
解决思路就一句话:"服务器记不住没关系,我们替它记下来"。具体做法:
- 用户登录成功后,服务器在自己的数据库里写一条"已登录"的记录(叫 Session)。
- 同时给浏览器发一个"小牌子"(叫 Cookie,里面装着一串号码
sessionid)。 - 之后浏览器每次发请求,都会自动把这张小牌子带上。
- 服务器一看小牌子上的号码,就能在自己的数据库里找回"哦,是 admin"。
取东西时,你把牌子一亮,工作人员就能从 38 号柜里把你的东西拿出来。
小牌子 = Cookie(sessionid),柜子里的东西 = Session 数据。
看一遍它怎么工作
下面是一次"登录→访问列表→退出"的完整过程。注意每一步浏览器发了什么、服务器干了什么:
POST
/article/login/携带 username/password
对了:在
django_session 表写一条记录 {'username':'admin'}sessionid=abc123...Set-Cookie: sessionid=abc123GET
/article/自动带上
Cookie: sessionid=abc123abc123 在 session 表里查到 username=admin→ 已登录,返回列表 HTML
GET
/article/logout/跳转到登录页
/article/Cookie 还在,但服务器那边记录已删
request.session['username'] = ...,退出就 request.session.flush(),剩下的不用操心。
Django 默认把 Session 存哪?
默认存在数据库的 django_session 表里。第8章你跑过 migrate,这张表已经建好。所以——
settings.py,直接在视图里写 request.session[...] 就能用。
三个 API 就够用了
Session 在 Django 里被封装成 request.session——一个"带数据库自动同步功能的字典"。下面三个 API 覆盖了 99% 的日常需要:
| 写法 | 作用 |
|---|---|
request.session['key'] = value |
存一笔。写完后框架自动同步到数据库,不用你手动保存 |
request.session.get('key') |
取出来。没存过返回 None,不会报错 |
request.session.flush() |
清空这个用户的全部 session(用来"退出登录") |
每个的小例子
# ① 存值——把当前登录的用户名记下来
# 用法和字典一模一样:用 [] 加键名,等号后面是要存的内容
request.session['username'] = 'admin'
# 也能存其他东西,比如用户 id、购物车列表、当前主题色...
request.session['cart'] = ['书', '笔']
request.session['theme'] = 'dark'
# ② 取值——记得用 .get() 而不是 []
# 推荐写法(安全):
name = request.session.get('username')
# 如果之前没存过,name 就是 None,程序不会崩
# 不推荐写法(如果没存过会抛 KeyError 异常,把页面搞 500):
# name = request.session['username']
# ③ 清空——彻底"忘记"这个用户
# 调用后:数据库那条记录被删,浏览器的 sessionid Cookie 也失效
# 一句话:注销专用
request.session.flush()
.get() 和 [] 的区别要记牢request.session['xxx']——如果 xxx 没存过,会抛异常,整个页面变成 500 错误。request.session.get('xxx')——如果没存过,悄悄返回 None,程序继续跑。"判断是否登录"这种场景,永远用
.get()。
动手实现登录系统(5 个小步骤)
下面把"登录—列表保护—退出"这个完整流程做出来。一共要改 4 个文件,新建 1 个文件:
步骤 1:写登录视图和退出视图
打开 article/views.py。保留第8章已有的 article_list,在文件下面追加两个新函数:
# 文件顶部的 import 如果第8章没加 redirect,这里补上
# redirect 是"让浏览器去访问另一个网址"的函数
from django.shortcuts import render, redirect
# ════════════════════════════════════════════
# 登录视图:处理 /article/login/ 这个地址
# ════════════════════════════════════════════
# 一个视图函数要处理两种请求:
# GET —— 用户在浏览器地址栏输入网址回车,浏览器来"取页面"
# POST —— 用户填完表单点了"登录"按钮,浏览器来"交数据"
# 我们要在同一个函数里把这两种情况都处理掉
def login_view(request):
# request.method 告诉我们这次是 GET 还是 POST(字符串)
if request.method == 'POST':
# ──── POST 分支:用户提交了账号密码 ────
# request.POST 是一个类似字典的对象,装着表单数据
# .get('username') 取出 name="username" 那个 input 的值
# 用 .get() 是为了:即使用户没填,也只是返回 None,不报错
username = request.POST.get('username')
password = request.POST.get('password')
# 校验账号密码——课堂演示用硬编码(写死在代码里)
# ⚠️ 实际项目千万不能这样写:要查数据库 + 密码加盐哈希
# 这里只是为了把 Session 这件事讲清楚,先不引入用户表
if username == 'admin' and password == '123456':
# ★★★ 整个登录机制的核心就在这一行 ★★★
# 把用户名存进 session,相当于在服务器记一笔"admin 登过了"
# Django 在背后会做三件事(我们不用管):
# 1. 在 django_session 表里建一条记录
# 2. 给浏览器塞一个 sessionid Cookie
# 3. 之后浏览器每次来,凭这个 Cookie 找回这条记录
request.session['username'] = username
# 登录成功,跳到文章列表页
# redirect 的本质:返回一个 302 响应,让浏览器去访问另一个网址
return redirect('/article/')
else:
# 账号或密码错了,重新渲染登录页,并附带错误提示
# 第二个参数是模板路径,第三个参数是给模板的数据(字典)
# 模板里就能用 {{ error }} 显示这条错误信息
return render(request, 'article/login.html',
{'error': '用户名或密码错误'})
# ──── GET 分支:用户第一次打开登录页 ────
# 走到这里说明 request.method 不是 POST
# 直接把空白的登录表单渲染给用户看
return render(request, 'article/login.html')
# ════════════════════════════════════════════
# 退出视图:处理 /article/logout/ 这个地址
# ════════════════════════════════════════════
def logout_view(request):
# flush() 做的事:
# 1. 删掉 django_session 表里这个用户的记录
# 2. 让浏览器那个 sessionid Cookie 失效
# 一句话:彻底"忘记"这个用户登过
request.session.flush()
# 退出后跳回登录页,整个流程闭合
return redirect('/article/login/')
/article/login/)会被访问两次——第一次是用户打开页面(GET),第二次是用户点登录按钮提交(POST)。
两次都用同一个函数处理,靠
request.method 判断走哪条路。这是 Django 表单的通用写法,后面所有带表单的视图都长这个样子。
步骤 2:给文章列表加"门卫"
现在只是写好了登录视图,但文章列表本身还在裸奔——不登录也能进。我们要给 article_list 加一段"通行证检查"。
把第8章的 article_list 改成下面这样(新增的就是开头那 3 行):
def article_list(request):
# ┌─────────── 新增的门卫逻辑 ───────────┐
# 从 session 取 username
# 登过 → 取到 'admin' → not 'admin' 是 False → 不进 if,继续往下走
# 没登 → 取到 None → not None 是 True → 进 if,跳登录页
if not request.session.get('username'):
return redirect('/article/login/')
# └─────────────────────────────────────┘
# 走到这里,说明用户已经登录
# Article.objects.all() 是第8章学的:取出 article 表所有记录
articles = Article.objects.all()
# 把文章列表 和 当前用户名 一起传给模板
# 这样模板里就能用 {{ username }} 显示"你好,admin"
return render(request, 'article/list.html',
{'articles': articles,
'username': request.session.get('username')})
门卫永远写在函数的第一段。
步骤 3:注册两条新路由
视图函数光写好不够,得告诉 Django"这个网址对应这个函数"。修改 article/urls.py:
from django.urls import path
from . import views
# urlpatterns 是一个列表,Django 启动时会扫一遍
# 每个 path('网址段', 视图函数) 表示一条对应关系
urlpatterns = [
# 注意:这里的网址是相对路径,前缀 /article/ 由项目级 urls.py 拼上
path('', views.article_list), # /article/ → 文章列表
path('login/', views.login_view), # /article/login/ → 登录页
path('logout/', views.logout_view), # /article/logout/ → 退出
]
步骤 4:新建登录页模板
在 article/templates/article/ 目录下新建一个 login.html。这是用户输入账号密码的页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
<style>
/* 把登录框居中、收窄,看着舒服 */
body { font-family: sans-serif; max-width:360px;
margin:80px auto; padding:0 20px; }
input { width:100%; padding:8px; margin:6px 0;
box-sizing:border-box; }
button { width:100%; padding:10px; background:#3B6B9A;
color:#fff; border:none; cursor:pointer; }
.err { color:#B83B2E; }
</style>
</head>
<body>
<h2>🔐 博客登录</h2>
<!-- method="post" 告诉浏览器:用 POST 方式提交,把数据塞在请求体里 -->
<!-- 没写 action,默认提交到当前网址(即 /article/login/)-->
<form method="post">
<!-- ★★ 这一行非常重要,少了 Django 会返回 403 错误 ★★ -->
<!-- {% csrf_token %} 会渲染成一个隐藏的 input,里面是一段随机字符串 -->
<!-- 作用:防止"跨站请求伪造"(别人写个假表单骗你点击) -->
<!-- Django 默认开启了 CSRF 防护,所有 POST 表单都得带它 -->
{% csrf_token %}
<!-- name 属性必须和 views.py 里 request.POST.get('xxx') 的名字对应 -->
<input name="username" placeholder="用户名">
<input name="password" type="password" placeholder="密码">
<button>登录</button>
</form>
<!-- 校验失败时,视图会传一个 error 变量过来 -->
<!-- {% if %} 块:有 error 才显示,没 error 就什么也不渲染 -->
{% if error %}
<p class="err">❌ {{ error }}</p>
{% endif %}
<p style="color:#888">课堂账号:admin / 123456</p>
</body>
</html>
{% csrf_token %}method="post",就必须在 form 内部写 {% csrf_token %}。忘了就会得到一个赤红色的 403 Forbidden 报错页面,提示 "CSRF verification failed"。
初学者一定会踩这个坑至少一次——记牢这一行的位置:紧跟在
<form> 后面。
步骤 5:在列表页加欢迎语和退出按钮
登录成功后,用户在文章列表页应该能看到"你好,admin"和一个"退出"链接。打开 article/templates/article/list.html,在原来的 <h1> 下面加一段:
<h1>📝 我的博客</h1>
<!-- ↓↓↓ 下面这段是新增的 ↓↓↓ -->
<!-- {{ username }} 显示视图里传来的当前用户名 -->
<!-- 点"退出登录"会触发 logout_view,把 session 清掉 -->
<p>
👋 你好,<strong>{{ username }}</strong>
| <a href="/article/logout/">退出登录</a>
</p>
🎉 完整流程测试一遍
1️⃣ 用无痕窗口打开 http://127.0.0.1:8000/article/
2️⃣ 应该自动跳到登录页 → 输入 admin / 123456 → 点登录
3️⃣ 跳回文章列表,看到"👋 你好,admin"
4️⃣ 点"退出登录" → 跳回登录页
5️⃣ 再访问 /article/ → 又被踢回登录页 ✅
- 403 Forbidden:登录页 form 里忘写
{% csrf_token %} - 登录后还是跳登录页:检查视图里写的是不是
session['username'] = ...(不是username = ...) - 退出后还能看列表:浏览器有缓存。用无痕窗口测,或按 Ctrl+F5 强制刷新
- 页面 500 报错:八成是
request.session['xxx']拿了一个没存过的 key——改成.get()
📋 本节小结
三个 API 速查
| 场景 | 代码 |
|---|---|
| 登录成功——记下用户身份 | request.session['username'] = username |
| "门卫"——判断是否登录 | if not request.session.get('username'): ... |
| 退出登录——清掉记录 | request.session.flush() |
记住这几个关键认识
- HTTP 是无状态的——每个请求都是独立的,服务器自己记不住。
- Session 在服务器端,Cookie 在浏览器端——Cookie 里只有一串号码,真正的数据在服务器。
- 取 session 用
.get()——避免 KeyError。 - POST 表单必须带
{% csrf_token %}——少一个就 403。 - 门卫写在视图最开头——查数据之前先验权限。
本课不讲、但你应该知道的
- Django 自带一个完整的用户系统(
django.contrib.auth),里面有User模型、密码加密、login()/logout()函数、@login_required装饰器。本课为了讲清 Session 本身,用了硬编码的最朴素写法。下一阶段你会用到那一套。 - Session 数据除了存数据库,还能存 Redis、文件、纯 Cookie——改
settings.py的SESSION_ENGINE一行字就能切换。
课堂练习
两组共 12 题:第一组考概念,第二组考代码细节。点选项即可看答案。
Session 概念与原理(6 题)
把"为什么需要 Session、它怎么工作"想清楚。
为什么 HTTP 协议本身处理不了"登录状态"?
Django 默认把 session 数据存到哪里?
django_session 表。也能改成 Redis、文件、纯加密 Cookie——看 settings.py 的 SESSION_ENGINE 配置。Cookie 和 Session 是什么关系?
用户登录成功,request.session['username'] = 'admin' 这一行执行后,Django 会做什么?
所有用 POST 方法提交的表单,都必须在 <form> 里写 {% csrf_token %},否则 Django 会返回 403。
<form> 后面。课堂里把账号密码硬编码在视图里(if username == 'admin' and password == '123456'),实际上线项目也应该这样写。
User 模型 + authenticate(),密码经过加盐哈希存储。今天硬编码只是为了把 Session 机制讲清楚,不引入用户表的复杂度。代码细节与实战(6 题)
把动手实现里的关键代码看明白。
怎样"安全"地从 session 取值,即使 key 不存在也不报错?
.get() 在 key 不存在时返回 None,不抛异常;和 Python 字典的 .get() 一个道理。[] 取不到会抛 KeyError 把页面搞成 500。"退出登录"用哪个方法最干净?
flush() 会清掉这个用户的所有 session 数据并让浏览器的 sessionid 失效——彻底注销。仅靠"关闭浏览器"不可靠,Django 默认 session 有效期 2 周。下面是 article_list 的代码片段:
def article_list(request):
articles = Article.objects.all()
if not request.session.get('username'):
return redirect('/article/login/')
return render(...)
这段代码有什么问题?
在登录视图 login_view 里,if request.method == 'POST': 这个判断的作用是?
request.method 分流处理——这是 Django 表单的标准写法。登录页 HTML 里的 <input name="username">,这个 name="username" 对应视图里哪一行?
input 的 name 属性就是表单数据的"键"。HTML 里写 name="username",Python 里就用 request.POST.get('username') 把它取出来。两边的字符串必须一字不差。视图里写 request.session.get('username'),如果用户没登录过,这一行会抛 KeyError 异常导致页面 500。
.get() 在 key 不存在时悄悄返回 None,不会抛异常。会抛 KeyError 的是 request.session['username'](用方括号)。这就是为什么判断登录态永远用 .get()。