Flask-爱家租房项目ihome-01-项目框架的搭建

摘要:
项目简介本项目是一个简单的租赁平台项目。房东可以在平台上发布自己的房源,租户可以搜索他们喜欢的房源并在一定时间内租用。主要功能模块包括用户模块(注册/登录/个人信息)、租房主页、房源列表页面、房源详情页面、房租预订页面、支付宝支付、,该项目属于前端和后端不完全分离。前端使用html+css+jquery,后端使用烧瓶框架。后端仅提供数据交互接口。前端负责编写页面的显示和操作功能
项目介绍

该项目是一个简易版的租房平台项目, 房东可以在平台上发布自己的房源, 房客可以搜索心仪的房源并进行一定时间的租赁. 主要功能模块包括用户模块(注册/登录/个人信息), 租房首页, 房屋列表页,房屋详情页, 房屋预订页, 支付宝支付等.

该项目属于不完全的前后端分离, 前端使用的是html+css+jquery, 后端使用的是flask框架, 后端只提供数据交互的接口, 页面的展示和操作功能都由前端负责编写, 前后端数据交互的格式为json.

flask项目目录的构建

flask的项目框架不想django那样默认搭建好了, 好处是我们可以自己灵活搭建, 坏处就是太灵活了, 官方也没有提供一套基本的搭建方案, 因此可能不同flask项目的目录结构都不太一样, 本项目采用的目录结构是参考django的项目目录结构来搭建的.

因为flask项目理论上是可以把所用到的东西都放在一个文件中的, 就像简单的hello world实例项目, 把启动/配置/页面展示/路由视图都放在一起, 这里我们先把本项目需要用到的基本组件都放在一个单一文件中, 然后再把文件按不同功能的组建拆分开, 就能够更好的理解较大的项目其项目目录是如何拆分的.

构建单一项目文件

首先创建一个项目目录, 名为FlaskIhome, 在目录下新建一个单一项目启动文件, 名为manager.py(命名方式是参考django的manager.py, 该文件最终负责的工作只是负责项目的启动, 其他逻辑的代码都会被拆分到其他文件中), 搭建最简单的flask项目, 添加最基础的配置

# manager.py
from flask import Flask

# 创建app
app = Flask(__name__)

class AppConfig:
    """app设置类"""
    DEBUG = True
    SECRET_KEY = 'akdkmamd1235jijg9123'

# 应用添加配置
app.config.from_object(AppConfig)

@app.route('/')
def index():
    return 'index page'

然后添加其他将会使用的组件配置, 如:sqlalchemy/redis/session/migrate/csrf等

from flask import Flask
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_session import Session
from flask_wtf import CSRFProtect
import redis

app = Flask(__name__)

class AppConfig:
    """app设置类"""
    DEBUG = True
    SECRET_KEY = 'akdkmamd1235jijg9123'
    # 远程服务器
    REMOTE_SERVER = 'alex-gcx.com'
    # 数据库设置
    SQLALCHEMY_DATABASE_URI = f'mysql://root:root@{REMOTE_SERVER}:3306/ihome'
    SQLALCHEMY_TRACK_MODIFICATIONS = True
    # redis配置
    REDIS_HOST = REMOTE_SERVER
    REDIS_PORT = 6379
    REDIS_CACHE_DB = 0  # 缓存数据库
    REDIS_SESSION_DB = 1  # session数据库
    # flask-session配置
    SESSION_TYPE = 'redis'
    SESSION_REDIS = redis.StrictRedis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_SESSION_DB)
    SESSION_USE_SIGNER = True
    PERMANENT_SESSION_LIFETIME = 86400  # session缓存时间, 单位:秒, 设置为1天

# 应用添加配置
app.config.from_object(AppConfig)
# 创建数据库链接
db = SQLAlchemy(app)
redis_connect = redis.StrictRedis(host=AppConfig.REDIS_HOST, port=AppConfig.REDIS_PORT, db=AppConfig.REDIS_DB)
# 将app的session设置添加到默认的session机制中
Session(app)
# 创建迁移对象
migrate = Migrate(app, db)
# 启用CSRF模块
CSRFProtect(app)

@app.route('/')
def index():
    return 'index page'

注:

  • 其中mysql和redis数据库都是在另一台服务器alex-gcx.com上.
  • mysql在sqlalchemy的扩展插件中配置, redis没有使用flaks的扩展插件, 而是使用原生的python连接redis的方式.
  • 启用了两个redis数据库, 0号数据库是用来存储一些业务上的缓存信息的, 1号数据库专门用来存放session信息的.

拆分单一项目文件

app的配置信息

首先我们拆分app的配置类AppConfig.

在当前目录FlaskIhome新建一个config.py文件, 将AppConfig类移到该文件中, 并稍作修改

# config.py
import redis

class BasicConfig:
    """app基础设置类"""
    SECRET_KEY = 'AKDKMAmd1235jijg9123'
    # 远程服务器
    REMOTE_SERVER = 'alex-gcx.com'
    # 数据库设置
    SQLALCHEMY_DATABASE_URI = f'mysql://root:root@{REMOTE_SERVER}:3306/ihome'
    SQLALCHEMY_TRACK_MODIFICATIONS = True
    # redis配置
    REDIS_HOST = REMOTE_SERVER
    REDIS_PORT = 6379
    REDIS_CACHE_DB = 0  # 缓存数据库
    REDIS_SESSION_DB = 1  # session数据库
    # flask-session配置
    SESSION_TYPE = 'redis'
    SESSION_REDIS = redis.StrictRedis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_SESSION_DB)
    SESSION_USE_SIGNER = True
    PERMANENT_SESSION_LIFETIME = 86400  # session缓存时间, 单位:秒, 设置为1天

class DevConfig(BasicConfig):
    """开发环境配置"""
    DEBUG = True

class ProdConfig(BasicConfig):
    """生产环境配置"""
    DEBUG = False

config_map = {
    'dev': DevConfig,
    'prod': ProdConfig
}

注:

  • 想定义两个配置类, 一个给开发环境使用DevConfig, 一个给生产环境(正式环境)使用ProdConfig
  • 因此将两个环境可能共用的配置都抽出来放到基类BasicConfig中, 这里是指简单模拟两套环境的设置, 并不一定这些参数两者环境都相同
  • 定义了一个字典映射config_map, 外部可以通过不用的key值使用不同的环境配置类

创建应用工厂

manager.py最终的用途只是提供项目的启动脚本, 其他逻辑它并不负责实现, 因此我们把项目的业务相关的逻辑都放在一个新建的名为ihome的python包目录下, 这样在整个项目目录FlaskIhome中, 只有三个文件: 项目的配置文件config.py/项目的启动文件manager.py/项目的业务文件ihome文件夹.

因为manager.py并不关心app具体是怎么创建的, app是怎么配置的. 所以我们引入工厂函数, 将app的创建逻辑封装到一个名为create_app的函数中, 同时将该函数定义在ihome目录下的__init__文件中, 这样在启动文件manager.py中导入ihome包的create_app方法就能创建app对象了, 而不用关心app的具体创建逻辑.

ihome.__nit__.py中定义create_app方法, 并导入添加配置类

# __init__.py
from flask import Flask
from config import config_map

# 创建应用工厂
def create_app(env):
    """
    创建app的工厂方法
    :param env: str 环境参数 ('dev'/'prod')
    :return: app
    """
    app = Flask(__name__)
    # 应用添加配置
    config_class = config_map.get(env)
    app.config.from_object(config_class)
    return app

# 在manager.py中导入该方法并创建app即可
from ihome import create_app
app = create_app('dev')

移动组件配置信息

migrate组件因为是用来迁移的, 可以继续放在manager.py中, 其他组件如sqlalchemy/redis/session/csrf等, 都需要和app进行绑定, 我们可以把这些组建在创建app的时候就进行绑定, 即移动到create_app方法中, 这样manager.py就变得非常干净了, 只剩下启动和迁移功能.

# manager.py
from flask_migrate import Migrate
from ihome import create_app
from ihome import db
# 创建app
app = create_app('dev')
# 创建迁移对象
migrate = Migrate(app, db)

那么在ihome.__init__文件中添加组件信息

# __init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_session import Session
from flask_wtf import CSRFProtect
import redis
from config import config_map

# 创建应用工厂
def create_app(env):
    """
    创建app的工厂方法
    :param env: str 环境参数 ('dev'/'prod')
    :return: app
    """
    app = Flask(__name__)
    # 应用添加配置
    config_class = config_map.get(env)
    app.config.from_object(config_class)

    # 绑定flask扩展
    # 创建数据库链接
    db = SQLAlchemy(app)
    # 创建redis连接
    redis_connect = redis.StrictRedis(host=config_class.REDIS_HOST, port=config_class.REDIS_PORT,
                                      db=config_class.REDIS_CACHE_DB)
    # 将session绑定为app中的设置
    Session(app)
    # 启用CSRF模块
    CSRFProtect(app)

    return app

但是这样会发现其中dbredis_connect对象不能只定义在create_app中, 因为在定义模型类时需要用到db对象, 在视图函数中也需要用到这两个对象, 因此这两个对象必须暴露在create_app方法外面供外部程序调用.

但是这两个对象在定义时又需要用到前面的app对象或者配置类config_class的信息, 因此不能直接把代码移到外面去. 这里就刚好有两种解决这个问题的方案:

  1. 对于db = SQLAlchemy(app), 因为使用的是flask-sqlalchemy, 这样的flask扩展一般都有两种定义方法:

    • 第一种即为上面写的这一种, 这样的写法是在定义db对象的同时与app进行了绑定
    • 第二种即为定义db对象时先不与app进行绑定, 等到app创建的时候再通过db对象的init_app()方法进行绑定, 即延迟了与app的绑定, 一般flask扩展都有init_app()方法
    # 创建数据库链接
    db = SQLAlchemy()
    # 创建应用工厂
    def create_app(env):
        app = Flask(__name__)
        # 绑定flask扩展
        db.init_app(app)
    
  2. 对于使用python原生的redis连接方式(即不是用的flask扩展), 可以现在外部定义一个redis_connect初始化为None, 在create_app中再通过global对前面定义的全局变量redis_connect进行赋值

    # 初始化redis连接
    redis_connect = None
    # 创建应用工厂
    def create_app(env):
        app = Flask(__name__)
        # 创建redis连接
        global redis_connect
        redis_connect = redis.StrictRedis(host=config_class.REDIS_HOST, port=config_class.REDIS_PORT,
                                          db=config_class.REDIS_CACHE_DB)
    

修改完成之后, __init__文件如下:

# __init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_session import Session
from flask_wtf import CSRFProtect
import redis
from config import config_map

# 创建数据库链接
db = SQLAlchemy()
# 初始化redis连接
redis_connect = None

# 创建应用工厂
def create_app(env):
    """
    创建app的工厂方法
    :param env: str 环境参数 ('dev'/'prod')
    :return: app
    """
    app = Flask(__name__)
    # 应用添加配置
    config_class = config_map.get(env)
    app.config.from_object(config_class)

    # 绑定flask扩展
    # 初始化绑定mysql数据库
    db.init_app(app)
    # 创建redis连接
    global redis_connect
    redis_connect = redis.StrictRedis(host=config_class.REDIS_HOST, port=config_class.REDIS_PORT,
                                      db=config_class.REDIS_CACHE_DB)
    # 将session绑定为app中的设置
    Session(app)
    # 启用CSRF模块
    CSRFProtect(app)

    return app

组件配置小总结:

  • 这个__init__文件主要向外暴露两方面的内容:

    • create_app, 创建应用的工厂方法, 里面封装创建应用的具体逻辑
    • 其他文件需要引入的一些扩展对象, 如db, redis_connect等
  • flask扩展插件有两种初始化方式, 以flask_sqlalchemy为例:

    • 创建db对象的同时传入app对象: db.SQLAlechmy(app)

    • 创建db对象时不传入app对象, 等到app对象创建出来后再调用db对象的init_app将app绑定

      db.SQLAlchemy()
      ...
      db.init_app(app)
      

      即先创建扩展对象, 再在创建app时将扩展对象与app绑定, 延迟绑定, 对于那些需要放在create_app外面的扩展对象, 就需要使用第二种方法, 即先创建, 后绑定app

  • 对于redis连接这种不属于flask扩展插件的对象, 可以先定义一个全局变量, 初始化为None, 在create_app的方法中再赋予具体的对象值, 也能达到延迟绑定的效果

  • 对于Session和CSRFProtect初始化的对象并不需要暴露在外面, 因为这两个对象只是赋予或者修改了一些app的机制, 后续程序使用时并不会调用这两个对象

模型类的创建(models.py)

在业务目录ihome中, 新建一个模型类文件models.py, 因为该项目用到的表并不是很多, 因此把所有的表都放在一个模型类文件中, 如果项目用到的表比较多, 可以像django一样分为多个不同功能模块的模型类文件.

最终分析的模型结构如下, 分别为 用户模型(User)/房屋模型(House)/房屋图片模型(House_Image)/房屋地区模型(Area)/所有设施模型(Facility)/房屋具体设施模型(House_Facility)/订单模型(Order):

Flask-爱家租房项目ihome-01-项目框架的搭建第1张

# models.py
from ihome import db
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash

class BasicModel(db.Model):
    """建表的基类"""
    __abstract__ = True

    id = db.Column(db.Integer, primary_key=True)
    created_date = db.Column(db.DateTime, nullable=False, default=datetime.now())
    updated_date = db.Column(db.DateTime, nullable=False, default=datetime.now(), onupdate=datetime.now())
    is_delete = db.Column(db.Boolean, nullable=False, default=False)

    
class Users(BasicModel):
    """用户模型类"""
    __tablename__ = 'ih_users'

    phone = db.Column(db.String(11), unique=True, nullable=False)
    password_hash = db.Column(db.String(256), nullable=False)
    name = db.Column(db.String(240), unique=True, nullable=False)
    image_url = db.Column(db.String(240))
    real_name = db.Column(db.String(30), unique=True)
    real_id_card = db.Column(db.Integer, unique=True)

    # 将密码password设置为方法属性, 该属性不能读取, 只能赋值
    @property
    def password(self):
        raise AttributeError('密码不允许被读取')

    #  Users.password='xxxx'赋值密码时, 将输入的密码加密后存入数据库中
    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    # 校验用户登录时输入的密码
    def check_password_hash(self, password):
        return check_password_hash(self.password_hash, password)

    # 友好展示模型类对象
    def __repr__(self):
        return self.name


class Areas(BasicModel):
    """城区模型类"""
    __tablename__ = 'ih_areas'

    name = db.Column(db.String(32), unique=True, nullable=False)

    # 友好展示模型类对象
    def __repr__(self):
        return self.name


class Houses(BasicModel):
    """房屋模型类"""
    __tablename__ = 'ih_houses'

    user_id = db.Column(db.Integer, db.ForeignKey('ih_users.id'), nullable=False)
    user = db.relationship('Users', backref='houses')
    area_id = db.Column(db.Integer, db.ForeignKey('ih_areas.id'), nullable=False)
    area = db.relationship('Areas', backref='houses')

    title = db.Column(db.String(240), nullable=False)
    price = db.Column(db.Integer, default=0)  # 单价,单位:分
    address = db.Column(db.String(512))  # 地址
    room_count = db.Column(db.Integer, default=1)  # 房间数目
    acreage = db.Column(db.Integer, default=0)  # 房屋面积
    unit = db.Column(db.String(32))  # 房屋单元, 如几室几厅
    capacity = db.Column(db.Integer, default=1)  # 房屋容纳的人数
    beds = db.Column(db.String(64))  # 房屋床铺的配置
    deposit = db.Column(db.Integer, default=0)  # 房屋押金
    min_days = db.Column(db.Integer, default=1)  # 最少入住天数
    max_days = db.Column(db.Integer, default=0)  # 最多入住天数,0表示不限制
    order_count = db.Column(db.Integer, default=0)  # 该房屋的历史订单数
    default_image_url = db.Column(db.String(240))  # 默认显示的图片

    # 友好展示模型类对象
    def __repr__(self):
        return self.title


class HouseImages(BasicModel):
    """房屋图片表"""
    __tablename__ = 'ih_house_images'

    house_id = db.Column(db.Integer, db.ForeignKey('ih_houses.id'), nullable=False)
    house = db.relationship('Houses', backref='images')
    image_url = db.Column(db.String(240), nullable=False)


# 房屋和设置表属于多对多关系, 官方推荐db.table的方式建立多对多关系
house_facilities = db.Table('ih_house_facilities',
                            db.Column('house_id', db.Integer, db.ForeignKey('ih_houses.id'), nullable=False),
                            db.Column('facility_id', db.Integer, db.ForeignKey('ih_facilities.id'), nullable=False))


class Facilities(BasicModel):
    """基础设置模型类"""
    __tablename__ = 'ih_facilities'

    name = db.Column(db.String(32), nullable=False)

    # 友好展示模型类对象
    def __repr__(self):
        return self.name


class Orders(BasicModel):
    """订单模型类"""
    __tablename__ = 'ih_orders'

    order_num = db.Column(db.String(30), unique=True, nullable=False, index=True)
    user_id = db.Column(db.Integer, db.ForeignKey('ih_users.id'), nullable=False)
    user = db.relationship('Users', backref='orders')
    house_id = db.Column(db.Integer, db.ForeignKey('ih_houses.id'), nullable=False)
    house = db.relationship('Houses', backref='orders')
    start_date = db.Column(db.Date, nullable=False)
    end_date = db.Column(db.Date, nullable=False)
    days = db.Column(db.Integer, nullable=False)
    price = db.Column(db.Integer, nullable=False)
    amount = db.Column(db.Integer, nullable=False)
    comment = db.Column(db.Text)
    status = db.Column(db.Enum('NEW', 'PAID', 'ACCEPTED', 'COMPLETED', 'REJECTED', 'CANCELLED'), default='NEW', index=True)

    # 友好展示模型类对象
    def __repr__(self):
        return self.order_num

注:

  • 需要导入之前创建的db对象, 模型对象类都需要继承db.Model
  • 定义了一个抽象基类, 将所有表都需要用到的共有字段定义在基类中, 并且需要定义__abstract__ = True才不会创建该基类的数据表
  • 在存储用户表的密码字段时, 想把存入表中的密码为加密后的字符串, 那么可以通过@property实现, 在@password.setter中赋值为加密后的值
  • 对于图片字段, 这里会使用第三方存储图片的服务, 所以存的是访问图片的url
  • 对于一对多的关系, 使用外键db.ForeignKey和关系db.relationship
  • 对于多对多的关系, 使用db.Table()的方式创建中间表
  • 使用db.Enum('x','y','z')可以实现字段的枚举

创建蓝图模块

一个大项目可以分为多个功能模块, 如这里的用户模块, 房屋模块, 订单模块等. 在django中每个模块可以使用app来进行定义, 而在flask中, app的概念就是整个项目的意思, 而对于每个小功能模块, flask引入了蓝图(Blueprint)这个概念.即对应着django中app的概念.

我们一般会在业务模块目录中(即本项目的ihome目录), 创建每个蓝图的目录, 因为本项目比较小, 模块没有那么复杂, 且模型类都定义在一个文件models.py中, 那么定义视图文件时, 就可以直接通过功能模块.py的方式区分不用模块, 即通过py文件分隔不同模块, 而不是通过文件夹目录的形式. 那么这里定义蓝图时我们就使用版本号来分隔, 因为互联网产品的版本更迭很快, 可能需要不同的版本同时运行, 因此我们可以不同的版本使用不同的蓝图来定义.

创建python包目录api_1_0, 即v1.0版本的代码, 一般在该目录下的__init__中来定义蓝图, 这样导入该包时就能导入其中定义的蓝图

# __init__.py
from flask import Blueprint

# 创建蓝图
api = Blueprint('api_1_0', __name__, url_prefix='/api/v1.0')

# 导入蓝图的视图
from . import users

注:

  • 蓝图可以理解为flask中app的子类, 如果创建app就如何创建蓝图
  • 蓝图的url前缀可以在定义蓝图时设置, 也可以在注册蓝图时设置
  • 需要在蓝图中导入视图文件才能让flask运行时解析到定义视图函数
  • 创建的蓝图对象名为api, 而该蓝图的名字一般和目录名相同为api_1_0, 即一个api_1_0可以创建多个蓝图对象

同时在api_1_0目录下创建用户模块的视图文件user.py, 注意导入模型类文件from ihome import models必须要写, 不然迁移数据库时会找不到我们前面定义的models.py中的模型类

# user.py
from . import api
from ihome import models

@api.route('/user')
def user():
    return 'user page'

蓝图定义完成后, 需要在app中注册一下, 就行django中注册app模块一样, 得要让项目知道定义了那些蓝图模块, 我们在创建app时注册蓝图, 编辑之前的create_app方法, 在最后导入并注册蓝图, 在注册时可以添加参数url_prefix设置蓝图的url模块前缀

# ihome/__init__.py
...
# 创建应用工厂
def create_app(env):
    ...
    # 注册蓝图, 蓝图最好是注册前才导入
    from ihome.api_1_0 import api
    app.register_blueprint(api)
    return app

注:

  • 导入蓝图的语句from ihome.api_1_0 import api不要写在文件的顶部, 写在顶部可能会造成循环导入的问题, 使得导入报错, 最好是写在注册语句的上面即可

执行数据库迁移命令

(flask) alex@alex:~/PycharmProjects/FlaskIhome$ export FLAKS_APP=manager.py
(flask) alex@alex:~/PycharmProjects/FlaskIhome$ flask db init
(flask) alex@alex:~/PycharmProjects/FlaskIhome$ flask db migrate
(flask) alex@alex:~/PycharmProjects/FlaskIhome$ flask db upgrade

启动程序加载过程

我们在终端设置FLASK_APP临时变量为manager,再运行flask run命令

(flask) alex@alex:~/PycharmProjects/FlaskIhome$ export FLAKS_APP=manager.py
(flask) alex@alex:~/PycharmProjects/FlaskIhome$ flask run
 * Serving Flask app "manager.py" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 145-609-607

打开浏览器, 输入网址http://127.0.0.1:5000/api/v1.0/user查看结果

Flask-爱家租房项目ihome-01-项目框架的搭建第2张

项目通过manager.py启动, 调用了create_app创建了app对象, 在创建app时绑定了组件的设置, 也导入并注册了api蓝图, 在导入蓝图的__init__文件中, 又导入了视图文件users.py, 在导入users.py中又导入了模型类文件models.py, 这样就把蓝图/视图函数/模型类串起来了.

创建静态文件/工具包/工具库等目录

ihome目录下, 创建三个文件夹, 分别为:

  • static: 静态文件目录, 用来存放前端html/css等一些固定的静态文件
  • utils: 公用程序包目录, 用来存放一些全局性的函数等公用程序
  • libs: 库文件目录, 用来存放一些外部第三方的功能源码等

最终项目框架目录如下:

.
├── config.py
├── ihome
│ ├── __init__.py
│ ├── api_1_0
│ │ ├── __init__.py
│ │ └── users.py
│ ├── libs
│ ├── models.py
│ ├── static
│ │ ├── css
│ │ ├── favicon.ico
│ │ ├── html
│ │ ├── images
│ │ ├── js
│ │ └── plugins
│ └── utils
├── manager.py
添加提供访问静态文件的蓝图

本项目前后端的代码都在同一个工程目录下, 前端的html/css/js等都放在ihome/static中, 因此默认情况下用户必须在浏览器的url地址栏输入前缀/static/html如访问租房首页:http://127.0.0.1:5000/static/html/index.html, 而我们更希望简化用户的输入, 如:http://127.0.0.1:5000或者http://127.0.0.1:5000/index

为了支持用户简化输入, 我们需要先解析出用户输入的url, 然后找到static中对应html文件, 并返回, 因此我们把这个转化功能设计成一个蓝图. 这个蓝图就直接写在一个py文件中即可. 该蓝图的作用就是: 解析用户输入的任何一个url, 并返回对应的静态html文件

自定义路由转换器

首先要做的就是解析用户输入的url, 可以通过url路由转换器来实现, 即app.route('/<string:html_file>'), 但是这样设置转换器则在/后面必须要有一个值才行, 否则就会报错, 但是我们想要的结果是用户可以在/后面不输入值, 那么就会默认跳转到主页. 所以我们可以使用正则自定义一个路由转换器.

创建路由转换器类

由于这个转换器可能不只这一个视图需要使用, 所以我们把它定义在公用文件目录utils下, 在utils下创建一个__init__.py文件, 标识这个utils是一个python包, 再创建一个commons.py文件

# utils/commons.py
from werkzeug.routing import BaseConverter

class ReConverter(BaseConverter):
    """自定义路由转换器"""

    def __init__(self, url_map, regex):
        # 调用父类的__init__方法
        super().__init__(url_map)
        # 将正则表达式参数保存到regex属性中
        self.regex = regex

在app中注册路由转换器

在创建app的工厂函数create_app中, 添加注册路由转换器的逻辑, 注册后就可以在蓝图的视图函数中使用了

# ihome/__init__.py
def create_app(env):
    ......
    # 注册自定义路由转换器
    from ihome.utils.commons import ReConverter
    app.url_map.converters['re'] = ReConverter
    ......
    return app

创建蓝图

ihome目录下, 添加一个新文件web_html.py, 注意使用转换器时, 冒号后面没有空格

# ihome/web_html.py
from flask import Blueprint, current_app

html = Blueprint('web_html', __name__)

@html.route('/<re(".*"):html_file>')  # 使用自定的路由转换器re
def get_html(html_file):
    """根据url中的html_file返回static路径下的html文件"""
    # 若/后面为空, 则默认返回主页index.html
    if html_file is None:
        html_file = 'index.html'
    # 若不是以html结尾, 则默认加上.html结尾
    if not html_file.endswith('.html'):
        html_file = html_file + '.html'
    # html文件在static下的html路径下, 而flask的静态路径只到static这一层, 因此需要在路径上拼上'html/'
    if html_file != 'favicon.ico':
        html_file = 'html/' + html_file
    # send_static_file能够将文件发送给浏览器
    return current_app.send_static_file(html_file)

注册蓝图

在创建app的工厂函数create_app中, 添加注册蓝图的逻辑

# ihome/__init__.py
def create_app(env):
    ......
    # 注册html蓝图
    from ihome.web_html import html
    app.register_blueprint(html)
    ......
    return app

浏览网址测试结果, 输入http://127.0.0.1:5000/

Flask-爱家租房项目ihome-01-项目框架的搭建第3张

CSRF攻击与防护

CSRF攻击

CSRF(Cross-site request forgery):跨站请求伪造,也可缩写为XSRF,简单的说CSRF攻击就是黑客利用浏览器的特性,在你不知情的情况下,以你的名义去发送转账、发邮件等一些恶意请求。

攻击示例

Flask-爱家租房项目ihome-01-项目框架的搭建第4张

CSRF攻击的必要条件

  1. 用户首先主动需要登录被攻击的网站,并在本地产生cookie,且没有退出登录(这个条件很容易满足,一般用户在正常访问了某个网站后并不会去主动退出每个网站的登录,且就算浏览器关闭了也并不一定会断开登录,因为可以保存登录状态,有时用户可能更希望只需登录一次就可以保存一段时间内都不需要再登录了)

  2. 用户得点击了黑客网站(这个条件肯定也是必须的,不然就不会被伪造攻击了,但是这个黑客网站并不一定只是一个一下就很容易辨认出来的网站,很有可能是一个界面和正规网站一样的网站,或者就是一个存在漏洞的经常被访问的正规网站,因此有可能用户点了一个来历不明的链接或者正常访问了一个有漏洞的正规网站,都可能存在被攻击的危险)

CSRF攻击的关键原理

  1. 利用了浏览器会自动带上登录后的cookie来再次访问网站的特性,实现了请求伪造。
  2. 但是需要注意的是黑客网站只能是使用了浏览器的cookie,而并不能够获取到cookie具体的值

CSRF防护

利用黑客网站并不能够获取到具体的cookie值的特点,可以在服务器端加上CSRF防护校验,让传过来请求在链接上也带上cookie的值(csrf_token)或者在请求体中带上cookie的值(csrf_token),然后服务器比对cookie中的值和csrf_token值是否一致,不一致则校验未通过。

  • 如果是用户正常的请求,在前端发送请求时是可以通过js拿的到cookie的值的, 因此可以保证两个cookie值相同,校验通过

  • 黑客网站虽然也可以使用js去拿cookie的值, 但是由于浏览器有同源策略的限制,一个服务器域名来源的脚本是无法获取到另一个服务器设置在浏览器中的资源的。所以黑客网站并不能知道具体的cookie值是多少, 因此由他强迫用户浏览器发送的请求中肯定就带不上正确的cookie值,校验不通过。

FLASK添加CSRF防护机制

在flask中使用扩展插件flask_wtf可以添加CSRF防护机制,在前面的应用创建工厂create_app中,我们就已经添加好了这个防护机制

from flask_wtf import CSRFProtect

def create_app(env):
    ......
    # 启用CSRF模块
    csrf = CSRFProtect(app)
    ....
    return app

# 或者先创建对象,再绑定app

csrf = CSRFProtect()
def create_app(env):
    ......
    # 启用CSRF模块
    csrf.init_app(app)
    ....
    return app

这里需要注意的是,这个机制只是完成了在服务器端的校验这个功能,至于给浏览器设置cookie和在发送请求中携带csrf_token还是需要我们手动设置。

在使用django或者flask自带的模板时,通常在form标签中添加一段类似{% csrf_token %}的代码就可以完成在请求中携带csrf_token。但是本项目是前后端分离的,不会是用到框架的模板功能,所以我们得自己设置csrf_token的cookie。

在返回html页面的时候设置cookie值,修改一下web_heml.py

# web_html.py
from flask import Blueprint, current_app, make_response
from flask_wtf import csrf

html = Blueprint('web_html', __name__)

@html.route('/<re(r".*"):html_file>')  # 使用自定的路由转换器re
def get_html(html_file):
    """根据url中的html_file返回static路径下的html文件"""
    # 若/后面为空, 则默认返回主页index.html
    if not html_file:
        html_file = 'index.html'
    # 若不是以html结尾, 则默认加上.html结尾
    if not html_file.endswith('.html'):
        html_file = html_file + '.html'
    # html文件在static下的html路径下, 而flask的静态路径只到static这一层, 因此需要在路径上拼上'html/'
    if html_file != 'favicon.ico':
        html_file = 'html/' + html_file

    # 生成response对象
    response = make_response(current_app.send_static_file(html_file))  # send_static_file能够将文件发送给浏览器
    # 生成csrf_token
    csrf_token = csrf.generate_csrf()
    # 设置cookie
    response.set_cookie('csrf_token', csrf_token)

    return response

注:

  • 使用flask-wtf插件的csrf.generate_csrf()方法能够生成csrf_token值
  • 为了设置cookie,需要使用flask的make_response形式设置cookie并返回响应

浏览器访问http://127.0.0.1:5000/register.html可以看到csrf_token的cookie已经设置好了,这样会每次访问页面时都生成新的csrf_token并刷新cookie

Flask-爱家租房项目ihome-01-项目框架的搭建第5张

在前端发送csrf_token

在flask自带的模板中,可以在form中添加{{ form.csrf_token }},而对于前后端分离的项目,不再使用自带的模板,则需要在发送的请求头中添加属性X-CSRFToken,或者在请求体中添加属性csrf_token

注意若在请求体中添加csrf_token,则必须限制请求体的提交格式为form类型,即使用原生的form表单提交,对于前后端采用json格式传输的情况,则只能在请求头中添加属性X-CSRFToken

在单个视图中取消csrf防护

有时可能需要在某些视图中单独取消csrf防护,做法是先导入与app绑定的csrf对象,再在视图函数上添加@csrf.exempt装饰器,如这里在注册页面取消csrf防护

from ihome import csrf

@csrf.exempt
@api.route('/users', methods=['POST'])
def register():
    pass
日志功能

使用的是python原生的日志模块,在ihome/__init__.py中添加日志功能

import logging
from logging.handlers import RotatingFileHandler

# 配置日志信息
# 设置日志的记录等级
logging.basicConfig(level=logging.INFO)
# 创建日志记录器,指明日志保存的路径、每个日志文件的最大大小、保存的日志文件个数上限
file_log_handler = RotatingFileHandler("logs/log", maxBytes=1024*1024*100, backupCount=10)
# 创建日志记录的格式              当前时间         日志等级     输入日志信息的文件名    函数名          行数        日志信息
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s')
# 为刚创建的日志记录器设置日志记录格式
file_log_handler.setFormatter(formatter)
# 为全局的日志工具对象(flask app使用的)添加日记录器
logging.getLogger().addHandler(file_log_handler)

日志信息记录在项目目录下的logs/log中,因此还需要手动创建logs目录,不然会报错说路径不存在,log文件会自动生成.

在其他视图函数中记录日志时

from flask import current_app
......
current_app.logger.error('错误信息')

免责声明:文章转载自《Flask-爱家租房项目ihome-01-项目框架的搭建》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Dubbo学习笔记2:Dubbo服务提供端与消费端应用的搭建Iframe自适应其页面高度[转]下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

防止一个用户登录多次的方法

在web开发时,有的系统要求同一个用户在同一时间只能登录一次,也就是如果一个用户已经登录了,在退出之前如果再次登录的话需要报错。 常见的处理方法是,在用户登录时,判断此用户是否已经在Application中存在,如果存在就报错,不存在的话就加到Application中(Application是所有Session共有的,整个web应用程序唯一的一个对象):...

CSS中加号、星号及其他符号的作用

很多朋友搞不清楚CSS中有哪些HACK,怎么使用,我翻译+整理了一下贴在这里。这篇文章是关于CSS的hacking技术。不要和微软专有的CSS属性“滤镜”混淆。   在理想世界里,正确的CSS应该在任何支持CSS的浏览器里工作良好。不幸的是,我们并不是生活在理想的世界里,浏览 器们布满了BUG和不一致。创建一个跨浏览器并且显示一致的页面,CSS开发者必须想...

Jenkins代码自动部署相关文档

环境 centos 7.0+ Java JDK 1.8+ jenkins 2.220 maven 3.0+ git 1.8+ 注意事项 一. linux 安装 JDK (jdk-8u201-linux-x64.tar.gz) 1.下载jdk 2.在/usr 目录下,新建 /java 目录, 3.在/java 目录下,新建/jdk目录, 4.把jdk-...

MVC+EF Core 完整教程20--tag helper详解

之前我们有一篇:“动态生成多级菜单”,对使用Html Helper做了详细讲述,并且自定义了一个菜单的 Html Helper: https://www.cnblogs.com/miro/p/5541086.html Html Helper是关联前后端的一个核心组件,后面的ASP.NET Core 又推出了Tag Helper, 作用和Html Helpe...

java汉字乱码解决办法

自从接触Java和JSP以来,就不断与Java的中文乱码问题打交道,现在终于得到了彻底的解决,现将我们的解决心得与大家共享。一、Java中文问题的由来Java的内核和class文件是基于unicode的,这使Java程序具有良好的跨平台性,但也带来了一些中文乱码问题的麻烦。原因主要有两方面,Java和JSP文件本身编译时产生的乱码问题和Java程序于其...

使用TCPDF插件生成pdf以及pdf的中文处理

目录(?)[+] 多种多样的pdf开发库 WKHTMLTOPDF 2FPDF 3TCPDF 中文问题 做了这么多年项目,以前只是在别人的项目中了解过PHP生成pdf文件,知道并不难,但是涉及到了pdf开发库,首先介绍pdf库。 多种多样的pdf开发库1.WKHTMLTOPDF wkhtmltopdf是一个很好的解决方案,基本上可以原样输出html...