flask-login 要四样:LoginManager、user_loader、UserMixin、一个 SECRET_KEY。 会话里只存一个用户 id,每个请求用 user_loader 把对象现查回来。 密码存哈希而不是密码本身;未登入时 current_user 是匿名对象,不是 None。
这一篇对应教材 3.3 到 3.3.3:启动 flask-login、建立注册表单类别、修改 User 模型。 这是全书的第二处三颗星,因为「状态」第一次真正进到应用里。
一、四样东西各管什么
| 要准备什么 | 写在哪 | 作用 |
|---|---|---|
| LoginManager() | web/init.py | 绑定应用,配登录页地址 |
| user_loader | web/init.py | 按 id 把用户取回来 |
| UserMixin | web/models.py | 给 User 补属性和方法 |
| SECRET_KEY | config | 签名会话,缺了登不上 |
四样缺一不可,而且缺哪一样报的错都不在同一个地方。 最隐蔽的是 SECRET_KEY:缺了它注册能成功、库里也有记录,但会话一写就抛错,看起来像「登入没反应」。 这四样就是 flask-login 的全部门槛。跨过去之后剩下的都是业务逻辑。
二、UserMixin 给的是什么
继承它之后,User 多了四个属性和一个方法:is_authenticated、is_active、is_anonymous、 is_anonymous 与 get_id()。 实测:这三个布尔值都是写死的,is_authenticated 是 True、is_active 是 True、is_anonymous 是 False。 get_id() 返回的是字符串 '1',不是整数 1。会话里存的就是这个字符串。
要做「封号不许登入」,重写 is_active 就行,别处一行不用改。 这正是 Mixin 这种设计的价值:钩子已经留好了。
三、user_loader:会话里只有一个 id
@login_manager.user_loader
def load_user(uid):
return db.session.get(User, int(uid))
实测 login_user 之后,会话里只有三个键:_user_id、_fresh、_id。其中 _user_id 的值就是字符串 '1'。 每个请求进来,flask-login 拿这个 id 调一次 user_loader,把对象查回来。 所以改了用户名,下一个请求立刻生效——不用让用户重新登入。 如果它把整个对象存进会话,改了名字还得等会话过期。
反过来也有代价:每个请求多一次数据库查询。用户量大的系统会加缓存。
四、密码怎么存
存哈希,不存密码。werkzeug 给了两个函数:generate_password_hash 与 check_password_hash。 实测同一个密码哈希两次,结果不一样,长度都是 162,前缀是 scrypt:32768:8:1。 因为每次都加了不同的盐。所以没法靠比对哈希值判断两个人密码是否相同,只能拿明文去 check。
前缀里那串数字是算法与参数。将来换算法,旧哈希照样能校验——因为它自己带着参数,不用你记。 表里那一列叫 password_hash,没有 password 这一列。这意味着密码丢了谁也找不回来,只能重置。
五、login_required 与那个 next
@bp.route("/users")
@login_required
def users():
return render_template("users.html")
装饰器一挂,未登入访问就返回 302,Location 是 /auth/login?next=%2Fusers。 那个 next 是 Flask-Login 自动加的:它记住了你本来想去哪,登入成功后把你送回去。 %2Fusers 就是 /users 的百分号编码。所以它可以当一个普通参数传回去,不用自己解析。
注意装饰器顺序:route 在上、login_required 在下。反了会报错,因为函数还没变成视图就被包住了。
六、未登入时 current_user 是谁
实测它的外壳是 LocalProxy,背后真正的对象是 AnonymousUserMixin,is_authenticated 是 False。 它不是 None。所以模板里可以直接写 current_user.is_authenticated, 不用先判空——这也是 Flask-Login 最贴心的一处设计。 外壳那层 LocalProxy 和 request、session 是同一个套路:只在有请求上下文时才有值。 在请求外面直接读它,报的错一模一样。
七、动手 Demo:flask-login:让应用认识谁在登录
这个 Demo 把 flask-login 接进上一篇的项目,然后逐项验证四样东西各自的作用。 最后一节故意不登入就去访问受保护页面,看它把人送去哪。
login_manager = LoginManager()
def create_app(config=None):
app = Flask(__name__)
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = "auth.login"
login_manager.login_message = "请先登入"
@login_manager.user_loader
def load_user(uid):
return db.session.get(User, int(uid))
return app
实跑输出:
--------------------------------------------------------------------
flask-login:让应用认识谁在登录
--------------------------------------------------------------------
① 四样东西各在哪
--------------------------------------------------------------------
| 要准备什么 | 写在哪 | 作用 |
|---|---|---|
| LoginManager() | web/__init__.py | 绑定应用,配登录页地址 |
| user_loader | web/__init__.py | 按 id 把用户取回来 |
| UserMixin | web/models.py | 给 User 补四个属性和一个方法 |
| SECRET_KEY | config | 签名会话,缺了它登不上 |
login_manager.login_view -> auth.login
login_manager.login_message -> 请先登入
! login_view 写的是端点名,不是地址。写成 "/auth/login" 会在跳转时抛 BuildError。
② UserMixin 补了什么
--------------------------------------------------------------------
User 的继承链 -> ['User', 'Model', 'Model', 'UserMixin', 'object']
MRO 里有 UserMixin 吗 -> True
is_authenticated -> True
is_active -> True
is_anonymous -> False
get_id() -> '1'(注意是字符串)
! 这四个属性全写死在 UserMixin 里,值都是固定 True 或 False。
要做「封号不许登入」,重写 is_active 就行,别处不用改。
③ user_loader 存的是 id,不是对象
--------------------------------------------------------------------
login_user 之后 session 里的键 -> ['_fresh', '_id', '_user_id']
_user_id 的值 -> '1'
! 会话里只有这一个字符串 id,每次请求用 user_loader 现查一次。
所以改了用户名,下一个请求立刻生效——不用重新登入。
④ 密码怎么存
--------------------------------------------------------------------
哈希长度 -> 162
前 22 个字符 -> scrypt:32768:8:1$SMWB1
同一个密码哈希两次,结果相同吗 -> False
校验对的密码 -> True
校验错的密码 -> False
! 两次结果不同,是因为每次加了不同的盐。所以没法靠比对哈希值判断两个人密码一样。
前缀里那串数字是算法与参数,换算法时旧密码照样能校验。
⑤ 没登入就想进受保护的页面
--------------------------------------------------------------------
GET /users -> 302,Location=/auth/login?next=%2Fusers
跟过去之后页面里有「请先登入」吗 -> True
⑥ 没登入时 current_user 是谁
--------------------------------------------------------------------
看到的外壳 -> LocalProxy
背后真正的对象 -> AnonymousUserMixin
is_authenticated -> False
! 它不是 None。
Flask-Login 塞了一个匿名用户对象进来,所以模板里可以直接写 current_user.is_authenticated,
不用先判空。
! 外面那层 LocalProxy 是 Flask 的上下文代理,和 request、
session 是同一个套路——它只在有请求上下文时才有值。
在请求外面直接读 current_user,报的错和读 request 一样。
第二节那个继承链里出现了两个 Model,一个是 Flask-SQLAlchemy 的、一个是 SQLAlchemy 的, UserMixin 排在它们后面。 第三节的会话键只有三个,_id 那一长串是会话自己的标识,不是用户 id。 第四节哈希长度 162、两次结果不同——这两条合起来才说明「加了盐」。 第六节那个 LocalProxy 会让人以为类型判断错了,其实背后是 AnonymousUserMixin。
30 秒实验
- 把 user_loader 改成返回 None,看登入之后会发生什么。
- 给 User 加一个
is_active属性返回 False,再登入一次,看 flask-login 怎么处理。 - 想一想:为什么把整个 User 对象塞进会话是个坏主意?
常见误区
- 忘了 SECRET_KEY:注册能成、库里有数据,但会话写不进去,表现像「登入没反应」。
- 把密码明文存进库:一旦库泄露就是全量泄露,而且没法补救。
- user_loader 返回整个对象:占会话空间,改了资料还要等过期。
- 装饰器顺序写反:login_required 写在 route 上面,启动就报错。
自测三问(点开对答案)
1. flask-login 在会话里存了什么? —— 只存用户 id(一个字符串),每个请求用 user_loader 现查。 2. 为什么同一个密码两次哈希结果不同? —— 每次加了不同的盐,所以只能拿明文去 check,不能比对哈希值。 3. 未登入时 current_user 是什么? —— 一个匿名用户对象(AnonymousUserMixin),is_authenticated 为 False,不是 None。四样东西齐了,下一篇写注册:查重、哈希、顺手建立会话。
- ☐ (本篇完,下一篇 3.3 注册功能:端点、模板与必须登入)