项目介绍
该项目是一个简易版的租房平台项目, 房东可以在平台上发布自己的房源, 房客可以搜索心仪的房源并进行一定时间的租赁. 主要功能模块包括用户模块(注册/登录/个人信息), 租房首页, 房屋列表页,房屋详情页, 房屋预订页, 支付宝支付等.
该项目属于不完全的前后端分离, 前端使用的是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
但是这样会发现其中db
和redis_connect
对象不能只定义在create_app
中, 因为在定义模型类时需要用到db
对象, 在视图函数中也需要用到这两个对象, 因此这两个对象必须暴露在create_app
方法外面供外部程序调用.
但是这两个对象在定义时又需要用到前面的app对象或者配置类config_class
的信息, 因此不能直接把代码移到外面去. 这里就刚好有两种解决这个问题的方案:
-
对于
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)
-
对于使用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):
# 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
查看结果
项目通过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/
CSRF攻击与防护
CSRF攻击
CSRF(Cross-site request forgery):跨站请求伪造,也可缩写为XSRF,简单的说CSRF攻击就是黑客利用浏览器的特性,在你不知情的情况下,以你的名义去发送转账、发邮件等一些恶意请求。
攻击示例
CSRF攻击的必要条件
-
用户首先主动需要登录被攻击的网站,并在本地产生cookie,且没有退出登录(这个条件很容易满足,一般用户在正常访问了某个网站后并不会去主动退出每个网站的登录,且就算浏览器关闭了也并不一定会断开登录,因为可以保存登录状态,有时用户可能更希望只需登录一次就可以保存一段时间内都不需要再登录了)
-
用户得点击了黑客网站(这个条件肯定也是必须的,不然就不会被伪造攻击了,但是这个黑客网站并不一定只是一个一下就很容易辨认出来的网站,很有可能是一个界面和正规网站一样的网站,或者就是一个存在漏洞的经常被访问的正规网站,因此有可能用户点了一个来历不明的链接或者正常访问了一个有漏洞的正规网站,都可能存在被攻击的危险)
CSRF攻击的关键原理
- 利用了浏览器会自动带上登录后的cookie来再次访问网站的特性,实现了请求伪造。
- 但是需要注意的是黑客网站只能是使用了浏览器的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
在前端发送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('错误信息')