使用Flask-Login实现用户认证
Flask-Login提供了实现用户认证需要的各类功能函数
- 安装
uv pip install flask-login - 初始化
- 实例化扩展类 LoginManager(app)
- 实现用户加载回调函数 @login_manager.user_loader
- 让User模型继承UserMixin类 class User(db.Model, UserMixin):
- 设置登录视图端点 login_manager.login_view = ‘login’,未登录用户访问对应URL会重定向到登录页,用login_manager.login_message来自定义错误提示消息。
核心对象
LoginManager, # Flask-Login 的核心管理器,负责初始化、配置登录视图等
UserMixin, # 简化用户模型,提供 Flask-Login 要求的核心属性 / 方法
login_user, # 登录核心函数:将用户 ID 存入加密 session(Cookie),标记用户为已登录
login_required,# 路由装饰器:访问该路由时,先检查current_user是否登录,未登录则跳转到配置的login_view
logout_user, # 登出核心函数:清除 session 中的用户 ID,current_user变回匿名用户
current_user # 表示当前登录用户的全局对象
from flask_login import LoginManager
from flask_login import UserMixin
login_manager = LoginManager(app)
@login_manager.user_loader
def load_user(user_id):
user = db.session.get(User, int(user_id))
return user
class User(db.Model, UserMixin):
# ...
- 登录
用户输入账号密码点击登录后,视图函数里拿到参数与用户注册时写入数据库中信息进行对比,如果相等则以用户模型类对象作为参数,调用Flask-Login提供的login_user()函数。@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') if not username or not password: flash('Invalid input.') return redirect(url_for('login')) user = db.session.execute(select(User).filter_by(username=username)).scalar()= if user is not None and user.validate_password(password): login_user(user) flash('Login success.') return redirect(url_for('index')) flash('Invalid username or password.') return redirect(url_for('login')) return render_template('login.html') - 登出
登出操作,调用 logout_user() 函数即可@app.route('/logout') @login_required def logout(): logout_user() flash('Goodbye.') return redirect(url_for('index'))
存储密码
用户注册时将密码散列值存入数据库
Flask 的依赖 Werkzeug 内置了用于生成和验证密码散列值的函数
- werkzeug.security.generate_password_hash() 用来为给定的密码生成密码散列值
- werkzeug.security.check_password_hash() 则用来检查给定的散列值和密码是否对应
class User(db.Model):
__tablename__ = 'user'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(20))
username: Mapped[str] = mapped_column(String(20))
password_hash: Mapped[Optional[str]] = mapped_column(String(255))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def validate_password(self, password):
return check_password_hash(self.password_hash, password)
password_hash字段长度原文档上是128位,MySQL 5.7+ 默认为严格模式,当存入超过128位字符时会直接抛出错误,非严格模式下会截断;SQLite不会强制校验长度,会完整存储,不截断,不抛出错误。
认证保护
- 视图保护,为不允许未登录用户访问的视图函数附加一个 login_required 装饰器
- 模版内容保护,过滤掉不对未登录用户展示的内容
{ % if current_user.is_authenticated % }
<不对未登录用户展示的内容>
{ % endif % }
流程
-> 初始化LoginManager
-> 定义带ID的用户模型
-> 实现load_user回调
-> 用login_user登录
-> 用current_user/@login_required验证登录状态
-> 用logout_user登出
原理
Flask-Login间接使用Cookie,不直接存用户信息,生成安全的session的前提是设置了secret_key
- Flask的session本质是加密的Cookie;
- Flask-Login将用户ID存入加密session;
- 设置Session数据:视图函数login中验证用户成功后,将user_id存入Session中,即session[‘user_id’] = user_id;
- 序列化Session数据:Flask将session字典(比如{‘user_id’: 1})序列化成JSON 字符串(默认格式)
- 生成签名(防篡改):Flask 会用SECRET_KEY和HMAC 算法(默认 HMAC-SHA1) 对序列化后的数据生成 “签名”;签名公式:签名 = HMAC(SECRET_KEY, 序列化数据, 算法=SHA1)
- 拼接并编码Cookie值:把 “签名 + 序列化数据” 拼接,再做 Base64 编码,最终生成 Cookie 的value;示例 cookie_value = Base64(签名 + “.” + 序列化数据),最终Cookie值可能长这样eyJ1c2VyX2lkIjoxfQ.Xw_8ZQ.9f8d7s6a5b4c3d2e1f0a9s8d7f6g5h4j3k2l1
- 响应时发送Set-Cookie头:# 示例 Set-Cookie: session=eyJ1c2VyX2lkIjoxfQ.Xw_8ZQ.9f8d7s6a5b4c3d2e1f0a9s8d7f6g5h4j3k2l1; HttpOnly; Secure; SameSite=Lax; Path=/
- 每次请求时,Flask先解密Cookie中的session,取出用户ID,再通过load_user回调函数加载用户对象,赋值给current_user;
- 从请求头中提取session Cookie值;
- Base64解码,拆分出”签名”和”序列化数据”;
- 用SECRET_KEY重新计算序列化数据的签名;
- 对比”重新计算的签名”和”Cookie中的签名”;若不一致,说明数据被篡改,Flask清空Session,视为无效请求;若一致,验证通过,则反序列化JSON字符串,恢复成session字典;
- Flask-Login 会读取session[‘user_id’],调用前面定义的load_user回调函数,加载用户对象并赋值给current_user,完成登录状态验证;
浏览器对Set-Cookie的默认处理
- 当浏览器收到服务端返回的Set-Cookie响应头后,解析Set-Cookie中的键值对(如session=abc123)和属性(如Domain、Path、Max-Age等);若未设置Max-Age/Expires,则Cookie是”会话Cookie”,将存储在内存中,关闭浏览器后立即失效;若设置了则为”持久Cookie”,存储到硬盘中,直到过期才被删除。
- 当浏览器向同一个服务器发起新请求时,会自动筛选符合条件的Cookie,添加到请求头的Cookie字段中;请求的URL必须匹配Cookie的Domain(域名)和Path(路径);多个Cookie会用;分割,拼在Cookie请求头中。
常见Cookie属性
- Domain=example.com: 仅向example.com或其子域名(如sub.example.com)请求时携带;未设置则默认是当前请求的域名(如www.example.com,仅向该域名请求时携带)
- Path=/admin:仅向example.com/admin/路径请求时携带;未设置则默认是当前请求的路径(如/login,仅向/login/请求时携带)
- Secure:仅在 HTTPS 请求中携带;HTTP 请求会忽略该 Cookie
- HttpOnly:禁止 JS 读取 Cookie(防 XSS),但不影响请求头携带(浏览器仍会自动加)
- SameSite=Lax:限制跨站请求携带:
-> Strict:完全禁止跨站携带(如从baidu.com跳转到example.com,请求不带 Cookie)
-> Lax:允许部分跨站 GET 请求携带(默认值)
-> None:允许跨站携带,但必须配合Secure - Max-Age=3600:Cookie 有效期(秒);过期后浏览器自动删除,不再携带
如前端通过AJAX发起跨域请求(比如从a.com请求b.com),浏览器默认不携带Cookie,需要前端 AJAX 请求开启withCredentials=true,且服务端响应头允许携带凭证
// 前端请求
fetch('https://b.com/api', {
credentials: 'include' // 等价于withCredentials=true
});
// 服务端响应头
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://a.com // 不能是*,必须指定具体域名
基于Token方式登录
