Flask----前后端不分离-登录

发布于:2025-02-10 ⋅ 阅读:(71) ⋅ 点赞:(0)

扩展模块

  • 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 BaseConfig

app = 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 EqualTo


class 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  GET

    return 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 db


class 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.data
        passwd = 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方式

  1. 设置cookie
# 在服务端返回响应时,设置cookie
res = render_template("index.html")
res.set_cookie("uid", str(user.id), max_age=3600)  # max_age 过期时间秒
return res
  1. 验证cookie
# 识别用户的登录,通过请求上下文对象request来获取
uid = request.cookies.get("uid")
if uid:
	# 查询用户对象
	user = UserModel.query.filter_by(id=uid).first()
	if user:
		# 已登录
		return render_template("index.html")
  1. 删除cookie
# 退出登录时,删除cookie
res = redirect(url_for("login"))
res.delete_cookie("uid")
return res
  1. flask中生成响应对象
res = Response("xxxx")
res = make_response("xxxx")
res = jsonify({})
res = redirect(xxx)
res = render_template("index.html")

session方式

  1. 设置方式
from flask import session  # 请求上下文对象

# 存储
session["uid"] = user.uid
return render_template("index.html")

  1. 验证方式
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")
  1. 删除方式
del session['uid']
# 或者
session.pop("uid", None)
  1. session的配置
# flask的配置类
class BaseConfig:
    # 连接uri
    SQLALCHEMY_DATABASE_URI = "mysql+pymysql://lauf:lauf123@localhost:3306/world"
    # flask-sqlalchemy追踪模型对象的修改,并且发送信号,需要额外的内存
    SQLALCHEMY_TRACE_MODIFICATIONS = False
    # 日志输出,用于调试
    SQLALCHEMY_ECHO = True

    HOST = "localhost"
    PORT = 5050
    DEBUG = True

    # 加密的秘钥,如session加密
    SECRET_KEY = "abcxx23"
    # session数据存储在Cookie中的key
    SESSION_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.data
        passwd = 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扩展,它处理登录、注销和长时间记住用户会话;
  • 提供对用户会话的管理;
  • 简化用户的登录、登出、登录检查;
  • 使用方式如下:
  1. 实例化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变量即可

  1. 定义模型类混入
    models/user_models.py中修改模型类定义。
from app import db, login_manager
from flask_login import UserMixin


class UserModel(db.Model, UserMixin):  # 继承UserMixin
	# ...其他不变

# 定义用户对象加载
@login_manager.user_loader
def loader_user(uid):
	return UserModel.query.get(int(uid))
	
  1. 视图中处理登入、登出
    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)
	

 

代码目录

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到