文章目录
- 扩展模块
- flask-wtf 的简单使用
- 定义用户数据模型
- 注册与登录
- 会话保持
- cookie方式
- session方式
- 基于session的登录
- flask-login实现登录、登出
- 代码目录
扩展模块
- flask-sqlalchmy,连接数据库
- flask-login,处理用户的登录,认证
- flask-session,会话保持,默认对用户数据加密,存储在客户端浏览器的cookie中,每次请求时携带cookie来识别用户;也可以存储在服务端的文件、数据库、缓存中;
- flask-wtf 处理表单数据,防止csrf攻击;flask-wtf文档
flask-wtf 的简单使用
基于flask-wtf 制作简单的注册、登录页面;
- 注册页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>注册页面</title><script src="/static/js/index.js"></script><link rel="stylesheet" href="/static/css/index.css">
</head>
<body><form method="POST" action="/reg">{{ form.csrf_token }}<br><h3>欢迎注册</h3><br>{{ form.uname.label }} {{ form.uname(size=20) }}<br>{{ form.passwd.label }} {{ form.passwd(size=20) }}<br>{{ form.confirm_passwd.label }} {{ form.confirm_passwd(size=20)}}<br><input type="submit" value="登录">{% if form.errors %}<ul class="errors">{% for error in form.errors %}<li>{{ error }}字段验证未通过</li>{% endfor %}</ul>{% endif %}
</form>
</body>
</html>
这里的form变量是flask渲染模板时,传入的表单对象。,form.csrf_token分别在表单中、cookie中生成一个秘钥,在提交表单时,cookie中的秘钥连同表单中的秘钥一同传给后端进行验证,验证通过则为合法的请求。
登录页面实现类似;
- flask后端定义表单子类、字段、验证器,app/_init_.py
# __author__ = "laufing"
import os
from flask import Flask
from .config import BaseConfigapp = Flask(__name__)
app.config.from_object(BaseConfig)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))# 模板地址
app.template_folder = os.path.join(BASE_DIR, "templates")
# 静态资源地址
app.static_url_path = "/static/"
app.static_folder = os.path.join(BASE_DIR, "static")# 导入表单
from flask_wtf import FlaskForm
# 导入字段
from wtforms import StringField, PasswordField, IntegerField, DateTimeField, BooleanField
from wtforms.validators import DataRequired, InputRequired # 必须输入
from wtforms.validators import EqualToclass RegForm(FlaskForm):uname = StringField("uname", validators=[InputRequired(), DataRequired()])passwd = PasswordField("passwd", validators=[DataRequired(), InputRequired()])confirm_passwd = PasswordField("confirm_passwd", validators=[EqualTo("passwd")])class LoginForm(FlaskForm):# 在服务端验证用户的输入uname = StringField("uname", validators=[DataRequired(), InputRequired()])passwd = PasswordField("passwd", validators=[DataRequired(), InputRequired()])
- flask后端定义视图,main.py
# __author__ = "laufing"
from app import app
from flask import render_template, jsonify, request, session, redirect, url_for
from app import LoginForm, RegForm@app.route("/reg", methods=["GET", "POST"])
def register():if request.method == "GET":form = RegForm()return render_template("reg.html", form=form) # 传入form对象渲染表单form = RegForm() # 接收request.form 表单数据if form.validate_on_submit():# 保存用户的信息,待实现return redirect('/login') # 重定向到 /login GETreturn render_template("reg.html", form=form)@app.route("/login", methods=["GET", "POST"])
def login():if request.method == "GET": # 返回登录页面return render_template("login.html", form=LoginForm())# 实例化表单对象(自动接收request.form里的数据), 接收用户的登录数据form = LoginForm()# 按添加的验证器,进行验证数据 & 是否POST请求if form.validate_on_submit():print(form.uname.data)print(form.passwd.data)return jsonify({"code": 200,"msg": "登录成功"})# 验证未通过时,显示错误信息return render_template("login.html", form=form)if __name__ == '__main__':app.run(host="localhost", port=5050, debug=True)
- 目录结构
定义用户数据模型
- 数据库mysql;
- 驱动 flask-sqlalchmy
- 迁移flask-migrate
- flask db init 初始化,生成迁移目录(仅一次);
- flask db migrate -m “描述”, 创建迁移脚本;
- flask db upgrade,应用迁移,创建数据库、表;
- flask db downgrade,降级
- flask db history,迁移历史
- 文件models/user_models.py
- 版本
- flask==2.0.3
- jinja2=3.1.1
- werkzeug == 2.0.3
- sqlalchemy==1.3
- flask-sqlalchemy==2.4.0
- pyjwt==2.0.0
- email-validator==1.0.5
- packaging==21.0
- flask-migrate==2.6.0
在窗口app对象的_init_.py文件中,添加如下:
from flask-sqlalchemy import SQLAlchemy
from flask-migrate import Migrate# ...db = SQLAlchemy()
db.init_app(app)
migrate = Migrate(app, db)
创建models目录/ user_models.py:
from app import dbclass UserModel(db.Model):__tablename__ = "user_t"# 必须设定主键id = db.Column(db.Integer, primary_key=True, autoincrement=True)uname = db.Column(db.String(50), unique=True)passwd = db.Column(db.String(64), unique=True)def __repr__(self):return self.uname
命令行下完成初始化、迁移:
flask db init
flask db migrate -m '生成迁移命令'
flask db upgrade
注册与登录
将注册登录的后端逻辑迁移到指定的模块,窗口routers目录及routers.py模块,仅为了便于管理路由与视图。
routers/routers.py 路由与视图函数:
# __author__ = "laufing"
# 注册路由与视图
from app import app, RegForm, LoginForm, db
from flask import request, session, render_template, send_file # request & session 请求上下文对象
from models.user_models import UserModel
from werkzeug.security import generate_password_hash, check_password_hash@app.route("/user/reg", methods=["GET", "POST"]) # 必须从/开始
def reg():if request.method == "GET":# 实例化表单对象form_obj = RegForm()# 渲染注册页面render_template("reg.html", form_obj=form_obj)# 实例化表单,接收request.form里的数据reg_form = RegForm()if reg_form.validate_on_submit(): # 验证通过# 保存用户的注册信息user = UserModel(uname=reg_form.uname.data, password=generate_password_hash(reg_form.passwd.data))# 通过会话保存db.session.add(user)db.session.commit()# 返回登录页面login_form = LoginForm()return render_template("login.html", form_obj=login_form)# 返回错误信息return render_template("reg.html", form_obj=reg_form)@app.route("/user/login", methods=["GET", "POST"]) # 必须从/开始
def login():if request.method == "GET":# 实例化表单对象form_obj = LoginForm()# 渲染注册页面render_template("login.html", form_obj=form_obj)# 实例化表单,接收request.form里的数据login_form = LoginForm()if login_form.validate_on_submit(): # 验证通过# 验证用户的信息uname = login_form.uname.datapasswd = login_form.passwd.data# 查询用户user = UserModel.query.filter_by(uname=uname).first()if user and check_password_hash(user.password, passwd):print("登录成功:", login_form.uname.data)# 会话保持session["uname"] = user.uname# 返回首页return render_template("index.html")else:login_form.custom_error = ["用户名或者密码错误"]return render_template("login.html", form_obj=login_form)# 返回错误信息return render_template("login.html", form_obj=login_form)
会话保持
- HTTP是一种无状态的协议,即每次请求都是独立的,服务器无法识别不同请求之间的关联性,无法记录与用户的会话状态;如用户登录认证后,下次请求可能还需要重新登陆认证,造成用户体验非常不好;
- 会话保持机制则通过在客户端和服务器之间维护一个会话标识,使得服务器可以识别并保持与客户端的连接状态;
- 会话保持的方式
- cookie
- session
- token
- cookie,登录的用户认证通过后,通过响应res对象来set_cookie(key, value, max_age=300, expires=“xxx”)设置cookie信息,并key-val形式按域隔离存储在客户端浏览器中(大小受限、不够安全),后续的请求每次携带cookie信息,服务端通过cookie识别用户已登录认证;
- session,用户认证通过后在服务器端存储用户的信息,并生成一个session id 返回给客户端浏览器,存储在cookie中,后续请求携带cookie中的session id 来让服务端识别会话状态;flask中的session数据是通过SECRET_KEY加密后存储在客户端浏览器的cookie中,每次请求携带cookie中的session实现自动识别会话状态;
- token,一般在前后端分离的项目中使用jwt token来会话保持;
cookie方式
- 设置cookie
# 在服务端返回响应时,设置cookie
res = render_template("index.html")
res.set_cookie("uid", str(user.id), max_age=3600) # max_age 过期时间秒
return res
- 验证cookie
# 识别用户的登录,通过请求上下文对象request来获取
uid = request.cookies.get("uid")
if uid:# 查询用户对象user = UserModel.query.filter_by(id=uid).first()if user:# 已登录return render_template("index.html")
- 删除cookie
# 退出登录时,删除cookie
res = redirect(url_for("login"))
res.delete_cookie("uid")
return res
- flask中生成响应对象
res = Response("xxxx")
res = make_response("xxxx")
res = jsonify({})
res = redirect(xxx)
res = render_template("index.html")
session方式
- 设置方式
from flask import session # 请求上下文对象# 存储
session["uid"] = user.uid
return render_template("index.html")
- 验证方式
uid = session.get("uid") # flask 底层自动从request.cookies.get("session")获取加密数据,自动验证
if uid:user = UserModel.query.filter_by(id=uid).first()if user:# 已登录认证return render_template("index.html")
- 删除方式
del session['uid']
# 或者
session.pop("uid", None)
- session的配置
# flask的配置类
class BaseConfig:# 连接uriSQLALCHEMY_DATABASE_URI = "mysql+pymysql://lauf:lauf123@localhost:3306/world"# flask-sqlalchemy追踪模型对象的修改,并且发送信号,需要额外的内存SQLALCHEMY_TRACE_MODIFICATIONS = False# 日志输出,用于调试SQLALCHEMY_ECHO = TrueHOST = "localhost"PORT = 5050DEBUG = True# 加密的秘钥,如session加密SECRET_KEY = "abcxx23"# session数据存储在Cookie中的keySESSION_COOKIE_NAME = "session_lauf"# session过期时间PERMANENT_SESSION_LIFETIME = 3600
基于session的登录
@app.route("/user/login", methods=["GET", "POST"]) # 必须从/开始
def login():if request.method == "GET":# 实例化表单对象form_obj = LoginForm()# 渲染注册页面render_template("login.html", form_obj=form_obj)# 实例化表单,接收request.form里的数据login_form = LoginForm()if login_form.validate_on_submit(): # 验证通过# 验证用户的信息uname = login_form.uname.datapasswd = login_form.passwd.data# 查询用户user = UserModel.query.filter_by(uname=uname).first()if user and check_password_hash(user.password, passwd):print("登录成功:", login_form.uname.data)# 会话保持session["uid"] = user.id# 返回首页return render_template("index.html")else:login_form.custom_error = ["用户名或者密码错误"]return render_template("login.html", form_obj=login_form)# 返回错误信息return render_template("login.html", form_obj=login_form)# 首页
@app.route("/index", methods=["GET"])
def index():# 已登录,返回首页uid = session["uid"] # 在获取uid时,flask底层从request.cookies.get("session_lauf")获取session数据,并解码if uid:# 查询用户对象user = UserModel.query.filter_by(id=uid).first()if user:return render_template("index.html", user=user)# 未登录,返回登录页面login_form = LoginForm()return render_template("login.html", form_obj=login_form)# 退出登录
@app.route("/user/logout", methods=["GET"])
def logout():session.pop("uid", None)# 或者del session["uid"]login_form = LoginForm()return render_template("login.html", form_obj=login_form)
flask-login实现登录、登出
- Flask-Login是一个Flask扩展,它处理登录、注销和长时间记住用户会话;
- 提供对用户会话的管理;
- 简化用户的登录、登出、登录检查;
- 使用方式如下:
- 实例化LoginManager对象
# 在 app/__init__.py 中实例化LoginManager
# app = Flask(__name__)
# 会话管理
login_manager = LoginManager()
login_manager.init_app(app) # app需要配置SECRET_KEY, 基于session实现会话保持; 同时login_manager会放置在current_app身上
login_manager.session_protection = 'strong' # 设置会话保护模式为强模式
login_manager.login_view = "login_view" # 若login_required验证不通过,则定向到登录视图 (不指定时,则会提示401 Unauthorized) 通过endpoint参数指定视图的名称;login_manager.login_message = "请登录" # 需要自己在登录的模板中处理显示
# login_required验证登录失败时,会重定向到login视图,在该视图中渲染模板并传入login_message信息
# if request.method == "GET":# 实例化表单对象
# form_obj = LoginForm()
# login_message = current_app.login_manager.login_message if 'next' in request.args else None
# print("登录失败:", request.args)# 渲染注册页面
# return render_template("login.html", form_obj=form_obj, login_message=login_message)
# 在模板中处理login_message变量即可
- 定义模型类混入
models/user_models.py中修改模型类定义。
from app import db, login_manager
from flask_login import UserMixinclass UserModel(db.Model, UserMixin): # 继承UserMixin# ...其他不变# 定义用户对象加载
@login_manager.user_loader
def loader_user(uid):return UserModel.query.get(int(uid))
- 视图中处理登入、登出
routers/routers.py
# 基于flask-login的登录
from flask_login import login_user, logout_user, login_required, current_user
# login_required 保护视图,必须登录才可以访问
# current_user 是flask-login提供的一个全局变量,在login_required保护的视图中获取当前登录的用户对象
# current_user.is_authenticated(检查用户是否已认证)、is_active(检查用户是否处于活动状态)、is_anonymous 是否匿名用户# 登录认证成功,保持会话 (在以上代码中设置session的地方,替换为如下)
login_user(user_obj)# 登出, 不传参
logout_user()# 保护视图
@app.route("/index", methods=["GET"], endpoint="index") # endpoint为视图的名称,用户url_for 逆向解析
@login_required
def index():return render_template("index.html", user=current_user)