赞
踩
Flask是一个用python编写的Web应用程序框架。Armin Ronacher带领一个名为Pocco的国际Python爱好者团队开发了Flask。Flask基于Werkzeug WSGI工具包和Jinja2模板引擎。两者都是Pocco项目。
Flask也被称为“microframework” ,因为它使用简单的核心,用extension增加其他功能。Flask没有默认使用的数据库、窗体验证工具。
Web Application Framework(Web应用程序框架)或简单的Web Framework(Web框架)表示一个库和模块的集合,使Web应用程序开发人员能够编写应用程序,而不必担心协议,线程管理等低级细节。
Web Server Gateway Interface(Web服务器网关接口,WSGI)已被用作Python Web应用程序开发的标准。 WSGI是Web服务器和Web应用程序之间通用接口的规范。
它是一个WSGI工具包,它实现了请求,响应对象和实用函数。 这使得能够在其上构建web框架。 Flask框架使用Werkzeug作为其基础之一。
jinja2是Python的一个流行的模板引擎。Web模板系统将模板与特定数据源组合以呈现动态网页。
Flask通常被称为微框架。 它旨在保持应用程序的核心简单且可扩展。Flask没有用于数据库处理的内置抽象层,也没有形成验证支持。相反,Flask支持扩展以向应用程序添加此类功能。一些受欢迎的Flask扩展将在本教程后续章节进行讨论。
- from flask import Flask
- app = Flask(__name__)
-
- @app.route('/')
- def hello_world():
- return 'Hello, World!'
-
- @app.route('/hello')
- def hello():
- return 'Hello2'
不要使用 flask.py 作为应用名称,这会与 Flask 本身发生冲突。
app.route(rule, options)
在上面的示例中,'/ ' URL与hello_world()函数绑定。因此,当在浏览器中打开web服务器的主页时,将呈现该函数的输出。
最后,Flask类的 run() 方法在本地开发服务器上运行应用程序。
app.run(host, port, debug, options)
所有参数都是可选的
| 序号 | 参数与描述 |
|---|---|
| 1 | host 要监听的主机名。 默认为127.0.0.1(localhost)。设置为“0.0.0.0”以使服务器在外部可用 |
| 2 | port 默认值为5000 |
| 3 | debug 默认为false。 如果设置为true,则提供调试信息 |
| 4 | options 要转发到底层的Werkzeug服务器。 |
上面给出的Python脚本是从Python shell执行的。
Python Hello.py
通过把 URL 的一部分标记为 <variable_name> 就可以在 URL 中添加变量。标记的 部分会作为关键字参数传递给函数。通过使用 <converter:variable_name> ,可以 选择性的加上一个转换器,为变量指定规则。
- from markupsafe import escape
-
- @app.route('/user/<username>')
- def show_user_profile(username):
- # show the user profile for that user
- return 'User %s' % escape(username)
-
- @app.route('/post/<int:post_id>')
- def show_post(post_id):
- # show the post with the given id, the id is an integer
- return 'Post %d' % post_id
-
- @app.route('/path/<path:subpath>')
- def show_subpath(subpath):
- # show the subpath after /path/
- return 'Subpath %s' % escape(subpath)

转换器类型:
| 类型 | 说明 |
|---|---|
int | 接受正整数 |
float | 接受正浮点数 |
path | 类似 string ,但可以包含斜杠 |
uuid | 接受 UUID 字符串 |
string | (缺省值) 接受任何不包含斜杠的文本 |
以下两条规则的不同之处在于是否使用尾部的斜杠。
- @app.route('/projects/')
- def projects():
- return 'The project page'
-
- @app.route('/about')
- def about():
- return 'The about page'
projects 是标准的URL ,尾部有一个斜杠,看起来就如同一个文件夹。 访问一个没有斜杠结尾的 URL 时 Flask 会自动进行重定向,帮你在尾部加上一个斜杠。
about 的 URL 没有尾部斜杠,因此其行为表现与一个文件类似。如果访问这个 URL 时添加了尾部斜杠就会得到一个 404 错误。这样可以保持 URL 唯一,并帮助 搜索引擎避免重复索引同一页面。
url_for() 函数用于构建指定函数的 URL。它把函数名称作为第一个 参数。它可以接受任意个关键字参数,每个关键字参数对应 URL 中的变量。未知变量 将添加到 URL 中作为查询参数。
为什么不在把 URL 写死在模板中,而要使用反转函数 url_for() 动态构建?
URL反转:根据视图函数名称得到当前所指向的url
url_for() 函数最简单的用法是以视图函数名作为参数,返回对应的url,还可以用作加载静态文件,如
- <link rel="stylesheet" href="{{url_for('static',filename='css/index.css')}}">
该条语句就是在模版中加载css静态文件.
url_for是用来拼接 URL 的: 可以使用程序 URL 映射中保存的信息生成 URL。url_for() 函数最简单的用法是以视图函数名作为参数, 返回对应的 URL。例如,在示例程序中 hello.py 中调用 url_for('index') 得到的结果是 /。
redirect 是重定向函数,输入一个URL后,自动跳转到另一个URL所在的地址
这里使用 test_request_context() 方法来尝试使用 url_for()。 test_request_context() 告诉 Flask 正在处理一个请求,而实际上也许我们正处在交互 Python shell 之中, 并没有真正的请求。参见 本地环境 。
- from flask import Flask, url_for
- from markupsafe import escape
-
- app = Flask(__name__)
-
- @app.route('/')
- def index():
- return 'index'
-
- @app.route('/login')
- def login():
- return 'login'
-
- @app.route('/guest/<guest>')
- def hello_guest(guest):
- return 'Hello %s as Guest' % guest
-
- @app.route('/user/<name>')
- def hello_user(name):
- if name =='admin':
- return render_template('/index.html', num=name)
- else:
- return redirect(url_for('hello_guest',guest = name))
-
- @app.route('/user/<username>')
- def profile(username):
- return '{}\'s profile'.format(escape(username))
-
- with app.test_request_context():
- print(url_for('index'))
- print(url_for('login'))
- print(url_for('login', next='/'))
- print(url_for('profile', username='John Doe'))

以上输出
- /
- /login
- /login?next=/
- /user/John%20Doe
Web 应用使用不同的 HTTP 方法处理 URL 。 缺省情况下,一个路由只回应 GET 请求。 可以使用 route() 装饰器的 methods 参数来处理不同的 HTTP 方法:
- from flask import request
-
- @app.route('/login', methods=['GET', 'POST'])
- def login():
- if request.method == 'POST':
- return do_the_login()
- else:
- return show_the_login_form()
如果当前使用了 GET 方法, Flask 会自动添加 HEAD 方法支持,并且同时还会 按照 HTTP RFC 来处理 HEAD 请求。同样, OPTIONS 也会自动实现。
动态的 web 应用也需要静态文件,一般是 CSS 和 JavaScript 文件。只要在你的包或模块旁边创建一个名为 static 的文件夹就行了。
使用特定的 'static' 端点就可以生成相应的 URL
url_for('static', filename='style.css')
这个静态文件在文件系统中的位置应该是 static/style.css 。
在 Python 内部生成 HTML 相当笨拙。因为你必须自己负责 HTML 转义, 以确保应用的安全。因此, Flask 自动为你配置 Jinja2 模板引擎。
使用 render_template() 方法可以渲染模板,你只要提供模板名称和需要 作为参数传递给模板的变量就行了。下面是一个简单的模板渲染例子:
- from flask import render_template
-
- @app.route('/hello/')
- @app.route('/hello/<name>')
- def hello(name=None):
- return render_template('hello.html', name=name)
Flask 会在 templates 文件夹内寻找模板。因此,如果你的应用是一个模块, 那么模板文件夹应该在模块旁边;如果是一个包,那么就应该在包里面:
情形 1 : 一个模块:
- /application.py
- /templates
- /hello.html
情形 2 : 一个包:
- /application
- /__init__.py
- /templates
- /hello.html
- <!doctype html>
- <title>Hello from Flask</title>
- {% if name %}
- <h1>Hello {{ name }}!</h1>
- {% else %}
- <h1>Hello, World!</h1>
- {% endif %}
在模板内部可以和访问 get_flashed_messages() 函数一样访问 request 、 session 和 g 1 对象。
模板在继承使用的情况下尤其有用。其工作原理参见 模板继承 方案文档。简单的说,模板继承可以使每个页面的特定元素(如页头、导航和页尾) 保持一致。
自动转义默认开启。因此,如果 name 包含 HTML ,那么会被自动转义。如果你可以 信任某个变量,且知道它是安全的 HTML (例如变量来自一个把 wiki 标记转换为 HTML 的模块),那么可以使用 Markup 类把它标记为安全的,或者在模板 中使用 |safe 过滤器。更多例子: 官方 Jinja2 模板文档 。。
下面 Markup 类的基本使用方法:
- >>> from markupsafe import Markup
- >>> Markup('<strong>Hello %s!</strong>') % '<blink>hacker</blink>'
- Markup(u'<strong>Hello <blink>hacker</blink>!</strong>')
- >>> Markup.escape('<blink>hacker</blink>')
- Markup(u'<blink>hacker</blink>')
- >>> Markup('<em>Marked up</em> » HTML').striptags()
- u'Marked up \xbb HTML'
什么是 g 对象?它是某个可以根据需要储存信息的 东西。更多信息参见 g 对象的文档和 使用 SQLite 3 文档。
对于 web 应用来说对客户端向服务器发送的数据作出响应很重要。在 Flask 中由全局 对象 request 来提供请求信息。如果你有一些 Python 基础,那么 可能 会奇怪:既然这个对象是全局的,怎么还能保持线程安全?答案是本地环境
某些对象在 Flask 中是全局对象,但不是通常意义下的全局对象。这些对象实际上是 特定环境下本地对象的代理。
设想现在处于处理线程的环境中。一个请求进来了,服务器决定生成一个新线程(或者 叫其他什么名称的东西,这个下层的东西能够处理包括线程在内的并发系统)。当 Flask 开始其内部请求处理时会把当前线程作为活动环境,并把当前应用和 WSGI 环境绑定到 这个环境(线程)。它以一种聪明的方式使得一个应用可以在不中断的情况下调用另一个 应用。
这对你有什么用?基本上你可以完全不必理会。这个只有在做单元测试时才有用。在测试 时会遇到由于没有请求对象而导致依赖于请求的代码会突然崩溃的情况。对策是自己创建 一个请求对象并绑定到环境。最简单的单元测试解决方案是使用 test_request_context() 环境管理器。通过使用 with 语句 可以绑定一个测试请求,以便于交互。例如:
- from flask import request
-
- with app.test_request_context('/hello', method='POST'):
- # now you can do something with the request until the
- # end of the with block, such as basic assertions:
- assert request.path == '/hello'
- assert request.method == 'POST'
另一种方式是把整个 WSGI 环境传递给 request_context() 方法:
- from flask import request
-
- with app.request_context(environ):
- assert request.method == 'POST'
通过使用 method 属性可以操作当前请求方法,通过使用 form 属性处理表单数据(在 POST 或者 PUT 请求 中传输的数据)。以下是使用上述两个属性的例子:
- from flask import request
-
- @app.route('/login', methods=['POST', 'GET'])
- def login():
- error = None
- if request.method == 'POST':
- if valid_login(request.form['username'],
- request.form['password']):
- return log_the_user_in(request.form['username'])
- else:
- error = 'Invalid username/password'
- # the code below is executed if the request method
- # was GET or the credentials were invalid
- return render_template('login.html', error=error)
当 form 属性中不存在这个键时会发生什么?会引发一个 KeyError 。 如果你不像捕捉一个标准错误一样捕捉 KeyError ,那么会显示一个400 错误页面。因此,多数情况下你不必处理这个问题。
要操作 URL (如 key=value )中提交的参数可以使用 args 属性:
searchword = request.args.get('key', '')
用户可能会改变 URL 导致出现一个 400 请求出错页面。因此, 推荐使用 get 或通过捕捉 KeyError 来访问 URL 参数。
Request对象的重要属性如下所列:
完整的请求对象方法和属性参见 Request 文档。
用 Flask 处理文件上传很容易,只要确保不要忘记在你的 HTML 表单中设置 enctype="multipart/form-data" 属性就可以了。否则浏览器将不会传送你的文件。
已上传的文件被储存在内存或文件系统的临时位置。你可以通过请求对象 files 属性来访问上传的文件。每个上传的文件都储存在这个 字典型属性中。这个属性基本和标准 Python file 对象一样,另外多出一个 用于把上传文件保存到服务器的文件系统中的 save() 方法。
- from flask import request
-
- @app.route('/upload', methods=['GET', 'POST'])
- def upload_file():
- if request.method == 'POST':
- f = request.files['the_file']
- f.save('/var/www/uploads/uploaded_file.txt')
- ...
如果想要知道文件上传之前其在客户端系统中的名称,可以使用 filename 属性。但是请牢记这个值是 可以伪造的,永远不要信任这个值。如果想要把客户端的文件名作为服务器上的文件名, 可以通过 Werkzeug 提供的 secure_filename() 函数:
- from flask import request
- from werkzeug.utils import secure_filename
-
- @app.route('/upload', methods=['GET', 'POST'])
- def upload_file():
- if request.method == 'POST':
- f = request.files['the_file']
- f.save('/var/www/uploads/' + secure_filename(f.filename))
- ...
更好的例子参见 上传文件 方案。
要访问 cookies ,可以使用 cookies 属性。可以使用响应 对象 的 set_cookie 方法来设置 cookies 。请求对象的 cookies 属性是一个包含了客户端传输的所有 cookies 的字典。在 Flask 中,如果使用 会话 ,那么就不要直接使用 cookies ,因为 会话 比较安全一些。
读取 cookies:
- from flask import request
-
- @app.route('/')
- def index():
- username = request.cookies.get('username')
- # use cookies.get(key) instead of cookies[key] to not get a
- # KeyError if the cookie is missing.
储存 cookies:
- from flask import make_response
-
- @app.route('/')
- def index():
- resp = make_response(render_template(...))
- resp.set_cookie('username', 'the username')
- return resp
注意, cookies 设置在响应对象上。通常只是从视图函数返回字符串, Flask 会把它们 转换为响应对象。如果你想显式地转换,那么可以使用 make_response() 函数,然后再修改它。
使用 延迟的请求回调 方案可以在没有响应对象的情况下设置一个 cookie 。
同时可以参见 关于响应 。
form.html
- {% extends 'common/base.html' %}
- {% block title %}
- 原生表单
- {% endblock %}
- {% block pagecontent %}
- {# <form action="{{ url_for('check') }}" method="post">#}
- <form action="{{ url_for('form') }}" method="post">
- <p>用户名: <input type="text" name="username" placeholder="请输入用户名" maxlength="12"></p>
- <p>密码: <input type="password" name="userpass" placeholder="请输入密码..."></p>
- <p><input type="submit" value="提交"></p>
- </form>
- {% endblock %}
manage.py
- @app.route('/form/')
- def form():
- return render_template('form1.html')
-
- #接收表单的数据
- @app.route('/check/',methods=['POST'])
- def check():
- print(request.form)
- return '提交过来了'
将俩个路由地址合并为同一个
- @app.route('/form/',methods=['GET','POST'])
- def form():
- if request.method == 'POST':
- print(request.form)
- return render_template('form1.html')
作用: 是一个用于表单处理的扩展库 提供表单的校验 csrf的功能
pip install flask-wtf
字段类型
| 字段名称 | 字段类型 |
|---|---|
| StringField | 普通文本字段 |
| PasswordField | 密码框 |
| SubmitField | 提交按钮 |
| TextAreaField | 多行文本域 |
| HiddenField | 隐藏域 |
| DateField | 日期 |
| DateTimeField | 日期时间 |
| IntegerField | 整形 |
| FloatFIeld | 浮点型 |
| RadioField | 单选字段 |
| SelectField | 下拉 |
| FileField | 文件上传字段 |
| BooleanField | 布尔字段 |
验证器
| 验证器 | 说明 |
|---|---|
| DataRequired | 必填 |
| Length | 长度 min max |
| IPAddress | IP地址 |
| 邮箱 | |
| URL | 地址 |
| Regexp | 正则匹配 |
| EqualTo | 验证俩个字段值的正确性 |
| NumberRange | 输入值的范围 min max |
实例
- from flask import Flask,render_template,request
- from flask_script import Manager
- from flask_bootstrap import Bootstrap
- #导入自定义表单类的基类
- from flask_wtf import FlaskForm
- #导入表单的字段
- from wtforms import StringField,PasswordField,SubmitField
- #导入验证器
- from wtforms.validators import Length,DataRequired
-
-
- app = Flask(__name__)
- bootstrap = Bootstrap(app)
- #加密种子 csrf需要使用
- app.config['SECRET_KEY'] = 'abcdedff'
- manager = Manager(app)
-
- class Login(FlaskForm):
- username = StringField('用户名',validators=[Length(min=6,max=12,message='用户名的长度为6~12为'),DataRequired(message='用户名不能为空!!!')])
- userpass = PasswordField('密码',validators=[Length(min=6,max=12,message='用户名的长度为6~12为'),DataRequired(message='密码不能为空!!!')])
- submit = SubmitField('登录')
-
- @app.route('/')
- def index():
- return render_template('index.html')
-
- @app.route('/form/',methods=['GET','POST'])
- def form():
- #将表单类实例化
- form = Login()
- if request.method == 'POST':
- #验证是否存在正确的csrftoken和 数据的正确性 如果都正确则为真
- if form.validate_on_submit():
- # print(request.form)
- print(form.username.data)
- return render_template('form2.html',form=form)

CRSF验证:这个机制其实就是在表单提交的过程中加一个随机字符串,只有当客户端和服务器的随机字符串一致,后端才执行,看似简单的一个机制却有效防止了CSRF攻击。
- {% extends 'common/base.html' %}
- {% block title %}
- 原生表单
- {% endblock %}
- {% block pagecontent %}
- <form action="{{ url_for('form') }}" method="post">
- <p>{{ form.csrf_token }}</p>
- <p>{{ form.username.label() }} {{ form.username(style='color:red;',class='userclass',placeholder='请输入用户名') }}
- {% if form.errors %}
- <span style="color:red;">{{ form.errors.username.0 }}</span>
- {% endif %}
- </p>
- <p>{{ form.userpass.label() }} {{ form.userpass() }}</p>
- <p>{{ form.submit() }}</p>
- </form>
- {% endblock %}

使用 bootstrap渲染表单
- {% import 'bootstrap/wtf.html' as wtf %}
- {% block pagecontent %}
- <div class="row">
- <div class="col-md-8">图片</div>
- <div class="col-md-4">{{ wtf.quick_form(form,action="",method="") }}
- </div>
- </div>
- {% endblock %}
- class Login(FlaskForm):
- ...
- def validate_username(self,field):
- # print(field)
- if field.data == 'zhangsan':
- # if self.username.data == 'zhangsan':
- raise ValidationError('该用户已存在')
注意:
validate_ 验证的字段名称 为固定格式
所有字段和验证器方法的使用
- class Login(FlaskForm):
- username = StringField('用户名',validators=[Length(min=6,max=12,message='用户名的长度为6~12为'),DataRequired(message='用户名不能为空!!!')])
- userpass = PasswordField('密码',validators=[Length(min=6,max=12,message='用户名的长度为6~12为'),DataRequired(message='密码不能为空!!!'),EqualTo('confirm',message='俩次密码输入不一致')])
- confirm = PasswordField('确认密码')
- info = TextAreaField('个人简介',validators=[Length(min=6,max=20,message='内容为6-20个长度'),DataRequired(message='内容不能为空')],render_kw={"style":"resize:none;",'placeholder':"请输入你此刻的感谢..."})
- hidde = HiddenField()
- birth = DateField('出生日期')
- birth = DateTimeField('出生日期')
- age = IntegerField('年龄',validators=[NumberRange(min=6,max=99,message='年龄为6~99岁')])
- money = FloatField()
-
- sex = RadioField(choices=[('w','女'),('m','男')])
- address = SelectField(choices=[('1001','北京'),('1002','上海'),('1003','天津')])
-
- file = FileField('文件上传')
-
- argee = BooleanField('请仔细阅读以上条款')
-
- ip = StringField('IPV4',validators=[IPAddress(message='请输入正确的ip地址')])
- url = StringField('url地址',validators=[URL(message='输入正确的url地址')])
- email = StringField('email',validators=[Email(message='请输入正确的邮箱地址')])
- preg = StringField('手机号码',validators=[Regexp('^[1][3-8][0-9]{9}$',flags=re.I,message='请输入正确的手机号码')])
- submit = SubmitField('登录')

当用户请求 或者有消息的显示 通过flash,get_flashed_messages 来进行操作
导入
- from flask import flash,get_flashed_messages
-
- class Login(FlaskForm):
- username = StringField('用户名',validators=[DataRequired(message='用户名不能为空')])
- userpass = PasswordField('密码',validators=[DataRequired(message='密码不能为空')])
- submit = SubmitField('登录')
-
- @app.route('/form/',methods=['GET','POST'])
- def form():
- form = Login()
- if form.validate_on_submit():
- if form.username.data == 'zhangsan' and form.userpass.data == '123456':
- flash('登录成功')
- return redirect(url_for('index'))
- else:
- flash('当前用户不存在')
- return render_template('user/login.html',form=form)

使用
- {% for message in get_flashed_messages() %}
- <div class="alert alert-danger" role="alert">{{ message }}</div>
- {% endfor %}
使用 redirect() 函数可以重定向。使用 abort() 可以 更早退出请求,并返回错误代码:
- from flask import abort, redirect, url_for
-
- @app.route('/')
- def index():
- return redirect(url_for('login'))
-
- @app.route('/login')
- def login():
- abort(401)
- this_is_never_executed()
上例实际上是没有意义的,它让一个用户从索引页重定向到一个无法访问的页面(401 表示禁止访问)。但是可以说明重定向和出错跳出是如何工作的。
缺省情况下每种出错代码都会对应显示一个黑白的出错页面。使用 errorhandler() 装饰器可以定制出错页面:
- from flask import render_template
-
- @app.errorhandler(404)
- def page_not_found(error):
- return render_template('page_not_found.html')
注意 render_template() 后面的 404 ,这表示页面对就的出错 代码是 404 ,即页面不存在。缺省情况下 200 表示:一切正常。
详见 错误处理 。
视图函数的返回值会自动转换为一个响应对象。如果返回值是一个字符串,那么会被 转换为一个包含作为响应体的字符串、一个 200 OK 出错代码 和一个 text/html 类型的响应对象。如果返回值是一个字典,那么会调用 jsonify() 来产生一个响应。以下是转换的规则:
jsonify 创建一个响应对象。(response, status) 、 (response, headers) 或者 (response, status, headers) 组成。 status 的值会重载状态代码, headers 是一个由额外头部值组成的列表 或字典。如果想要在视图内部掌控响应对象的结果,那么可以使用 make_response() 函数。
设想有如下视图:
- @app.errorhandler(404)
- def not_found(error):
- return render_template('error.html'), 404
可以使用 make_response() 包裹返回表达式,获得响应对象,并对该对象 进行修改,然后再返回:
- @app.errorhandler(404)
- def not_found(error):
- resp = make_response(render_template('error.html'), 404)
- resp.headers['X-Something'] = 'A value'
- return resp
JSON 格式的响应是常见的,用 Flask 写这样的 API 是很容易上手的。如果从视图 返回一个 dict ,那么它会被转换为一个 JSON 响应。
- @app.route("/me")
- def me_api():
- user = get_current_user()
- return {
- "username": user.username,
- "theme": user.theme,
- "image": url_for("user_image", filename=user.image),
- }
如果 dict 还不能满足需求,还需要创建其他类型的 JSON 格式响应,可以使用 jsonify() 函数。该函数会序列化任何支持的 JSON 数据类型。 也可以研究研究 Flask 社区扩展,以支持更复杂的应用。
- @app.route("/users")
- def users_api():
- users = get_all_users()
- return jsonify([user.to_json() for user in users])
除了请求对象之外还有一种称为 session 的对象,允许你在不同请求 之间储存信息。这个对象相当于用密钥签名加密的 cookie ,即用户可以查看你的 cookie ,但是如果没有密钥就无法修改它。
使用会话之前你必须设置一个密钥。举例说明:
- from flask import Flask, session, redirect, url_for, request
- from markupsafe import escape
-
- app = Flask(__name__)
-
- # Set the secret key to some random bytes. Keep this really secret!
- app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
-
- @app.route('/')
- def index():
- if 'username' in session:
- return 'Logged in as %s' % escape(session['username'])
- return 'You are not logged in'
-
- @app.route('/login', methods=['GET', 'POST'])
- def login():
- if request.method == 'POST':
- session['username'] = request.form['username']
- return redirect(url_for('index'))
- return '''
- <form method="post">
- <p><input type=text name=username>
- <p><input type=submit value=Login>
- </form>
- '''
-
- @app.route('/logout')
- def logout():
- # remove the username from the session if it's there
- session.pop('username', None)
- return redirect(url_for('index'))

这里用到的 escape() 是用来转义的。如果不使用模板引擎就可以像上例 一样使用这个函数来转义。
如何生成一个好的密钥?
生成随机数的关键在于一个好的随机种子,因此一个好的密钥应当有足够的随机性。 操作系统可以有多种方式基于密码随机生成器来生成随机数据。使用下面的命令 可以快捷的为 Flask.secret_key ( 或者 SECRET_KEY )生成值:
- python -c 'import os; print(os.urandom(16))'
- b'_5#y2L"F4Q8z\n\xec]/'
基于 cookie 的会话的说明: Flask 会取出会话对象中的值,把值序列化后储存到 cookie 中。在打开 cookie 的情况下,如果需要查找某个值,但是这个值在请求中 没有持续储存的话,那么不会得到一个清晰的出错信息。请检查页面响应中的 cookie 的大小是否与网络浏览器所支持的大小一致。
除了缺省的客户端会话之外,还有许多 Flask 扩展支持服务端会话。
一个好的应用和用户接口都有良好的反馈,否则到后来用户就会讨厌这个应用。 Flask 通过闪现系统来提供了一个易用的反馈方式。闪现系统的基本工作原理是在请求结束时 记录一个消息,提供且只提供给下一个请求使用。通常通过一个布局模板来展现闪现的 消息。
flash() 用于闪现一个消息。在模板中,使用 get_flashed_messages() 来操作消息。完整的例子参见 消息闪现 。
有时候可能会遇到数据出错需要纠正的情况。例如因为用户篡改了数据或客户端代码出错 而导致一个客户端代码向服务器发送了明显错误的 HTTP 请求。多数时候在类似情况下 返回 400 Bad Request 就没事了,但也有不会返回的时候,而代码还得继续运行 下去。
这时候就需要使用日志来记录这些不正常的东西了。自从 Flask 0.3 后就已经为你配置好 了一个日志工具。
以下是一些日志调用示例:
- app.logger.debug('A value for debugging')
- app.logger.warning('A warning occurred (%d apples)', 42)
- app.logger.error('An error occurred')
logger 是一个标准的 Logger Logger 类, 更多信息详见官方的 logging 文档。
更多内容请参阅 应用错误处理 。
如果想要在应用中添加一个 WSGI 中间件,那么可以用应用的 wsgi_app 属性来 包装。例如,假设需要在 Nginx 后面使用 ProxyFix 中间件,那么可以这样做:
- from werkzeug.middleware.proxy_fix import ProxyFix
- app.wsgi_app = ProxyFix(app.wsgi_app)
用 app.wsgi_app 来包装,而不用 app 包装,意味着 app 仍旧指向你 的 Flask 应用,而不是指向中间件。这样可以继续直接使用和配置 app 。
Flask-Login 和其他 Flask 组件并没有太大区别,有必要开始之前了解下用户登录的步骤:
Session ),并返回给用户一个会话号( Session id )Session id )确定用户是否有效依据以上步骤,我们设计一个应用场景,作为实现:
安装
pip install flask-login
先实例化 login_manager 对象,然后用它来初始化应用:
- from flask import Flask
- from flask_login import LoginManager
- # ...
- app = Flask(__name__) # 创建 Flask 应用
-
- app.secret_key = 'abc' # 设置表单交互密钥
-
- login_manager = LoginManager() # 实例化登录管理对象
- login_manager.init_app(app) # 初始化应用
- login_manager.login_view = 'login' # 设置用户登录视图函数 endpoint
secret_key,以防跨域攻击( CSRF )login_manager 的 login_view 属性,指定登录页面的视图函数 (登录页面的 endpoint),即验证失败时要跳转的页面,这里设置为登录页用户数据
要做用户验证,需要维护用户记录,为了方便演示,使用一个全局列表 USERS 来记录用户信息,并且初始化了两个用户信息:
- from werkzeug.security import generate_password_hash
- # ...
- USERS = [
- {
- "id": 1,
- "name": 'lily',
- "password": generate_password_hash('123')
- },
- {
- "id": 2,
- "name": 'tom',
- "password": generate_password_hash('123')
- }
- ]
用户信息只包含最基本的信息:
name 为登录用户名password 为登录密码,幸运的是模块 werkzeug.security 提供了 generate_password_hash 方法,使用 sha256 加密算法将字符串变为密文id 为用户识别码,相当于主键基于用户信息,定义两方法,用来创建( create_user )和获取( get_user )用户信息:
- from werkzeug.security import generate_password_hash
- import uuid
- # ...
- def create_user(user_name, password):
- """创建一个用户"""
- user = {
- "name": user_name,
- "password": generate_password_hash(password),
- "id": uuid.uuid4()
- }
- USERS.append(user)
-
- def get_user(user_name):
- """根据用户名获得用户记录"""
- for user in USERS:
- if user.get("name") == user_name:
- return user
- return None

create_user 接受用户名和密码,创建用户记录,对密码明文进行加密,并添加用户 ID (使用 uuid 模板的 uuid4 方法生成一个全球唯一码),存储到 USERS 列表中get_user 接受用户名,从 USERS 列表中查找用户记录,没有返回空用户类
下面创建一个用户类,类维护用户的登录状态,是生成 Session 的基础,Flask-Login 提供了用户基类 UserMixin,方便定义自己的用户类,我们定义一个 User:
- from flask_login import UserMixin # 引入用户基类
- from werkzeug.security import check_password_hash
- # ...
- class User(UserMixin):
- """用户类"""
- def __init__(self, user):
- self.username = user.get("name")
- self.password_hash = user.get("password")
- self.id = user.get("id")
-
- def verify_password(self, password):
- """密码验证"""
- if self.password_hash is None:
- return False
- return check_password_hash(self.password_hash, password)
-
- def get_id(self):
- """获取用户ID"""
- return self.id
-
- @staticmethod
- def get(user_id):
- """根据用户ID获取用户实体,为 login_user 方法提供支持"""
- if not user_id:
- return None
- for user in USERS:
- if user.get('id') == user_id:
- return User(user)
- return None

USERS 列表中的一个元素,用来初始化成员变量get_id 方法返回用户实例的 ID,这是必须实现的,不然 Flask-Login 将无法判断用户是否被验证get 是个静态方法,即可以通过类之间调用,是为了在获取验证后的用户实例时用的,必须接受参数 ID,返回 ID 所以对应的用户实例verify_password 方法接受一个明文密码,与用户实例中的密码做校验,将被用在用户验证的判断逻辑中有了用户类,并且实现了 get 方法,就可以实现 login_manager 的 user_loader 回调函数了,user_loader 的作用是根据 Session 信息加载登录用户,它根据用户 ID,返回一个用户实例:
- @login_manager.user_loader # 定义获取登录用户的方法
- def load_user(user_id):
- return User.get(user_id)
页面包括后台和展现(可以理解成前台)两部分
后台
需要定义一个 Form 类,用来设置页面的元素和规则:
- from wtforms import StringField, PasswordField
- from wtforms.validators import DataRequired, EqualTo
- # ...
- class LoginForm(FlaskForm):
- """登录表单类"""
- username = StringField('用户名', validators=[DataRequired()])
- password = PasswordField('密码', validators=[DataRequired()])
然后定义一个用户登录的视图函数 login:
- from flask import render_template, redirect, url_for, request
- from flask_login import login_user
- # ...
- @app.route('/login/', methods=('GET', 'POST')) # 登录
- def login():
- form = LoginForm()
- emsg = None
- if form.validate_on_submit():
- user_name = form.username.data
- password = form.password.data
- user_info = get_user(user_name) # 从用户数据中查找用户记录
- if user_info is None:
- emsg = "用户名或密码密码有误"
- else:
- user = User(user_info) # 创建用户实体
- if user.verify_password(password): # 校验密码
- login_user(user) # 创建用户 Session
- return redirect(request.args.get('next') or url_for('index'))
- else:
- emsg = "用户名或密码密码有误"
- return render_template('login.html', form=form, emsg=emsg)

分析下视图函数的逻辑:
GET 和 POST 方法form.validate_on_submit() 可以判断用户是否完整的提交了表单,只对 POST 有效,所以可以用来判断请求方式POST 请求,获取提交数据,通过 get_user 方法查找是否存在该用户login_user 方法创建用户 Session,然后跳转到请求参数中 next 所指定的地址或者首页 (不用担心如何设置 next,还记得上面设置的 login_manager.login_view = 'login' 吗? 对,未登录访问时,会跳转到 login,并且带上 next 查询参数)POST 请求,或者未经过验证,会显示 login.html 模板渲染后的结果前台
在 templates 模板下创建登录页面的模板 login.html:
- {% macro render_field(field) %} <!-- 定义字段宏 -->
- <dt>{{ field.label }}:
- <dd>{{ field(**kwargs)|safe }}
- {% if field.errors %}
- <ul class=errors>
- {% for error in field.errors %}
- <li>{{ error }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- </dd>
- {% endmacro %}
-
- <!-- 登录表单 -->
- <form method="POST">
- {{ form.csrf_token }}
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- {% if emsg %} <!-- 如果有错误信息 则显示 -->
- <h3> {{ emsg }}</h3>
- {% endif %}
- <input type="submit" value="登录">
- </form>

render_field 是 Jinja2 模板引擎的宏,接受表单字段将其渲染成 Html 代码,并格式化错误信息emsg 错误信息单独做了处理,如果存在会显示出来form 中并没有 action 属性,默认为当前路径为了方便演示,将首页作为需要验证的页面,通过验证将看到登录者欢迎信息,页面上还有个登出链接
首页视图函数 index:
- from flask import render_template, url_for
- from flask_login import current_user, login_required
- # ...
- @app.route('/') # 首页
- @login_required # 需要登录才能访问
- def index():
- return render_template('index.html', username=current_user.username)
@login_required 会做用户登录检测,如果没有登录要方法此视图函数,就被跳转到 login 接入点( endpoint )current_user 是当前登录者,是 User 的实例,是 Flask-Login 提供全局变量( 类似于全局变量 g )username 是模板中的变量,可以将当前登录者的用户名传入 index.html 模板首页模板 index.html:
- <h1>欢迎 {{ username }}!</h1>
- <a href='{{ url_for('logout')}}'>登出</a>
登出视图函数 logout:
- from flask import redirect, url_for
- from flask_login import logout_user
- # ...
- @app.route('/logout') # 登出
- @login_required
- def logout():
- logout_user()
- return redirect(url_for('login'))
@login_requiredlogout_user 方法和 login_user 相反,由于注销用户的 Session终于可以试试了,加上启动代码:
- if __name__ == '__main__':
- app.run(debug=True)
启动项目,如果一切正常将看到类似的反馈
上面的演示了,已存在用户登录的情况,不存在用户需要完成注册才能登录。
注册功能和登录很类似,页面上多了密码确认字段,并且需要验证两次输入的密码是否一致,后台逻辑是:如果用户不存在,且通过检验,将用户数据保存到 USERS 列表中,跳转到 login 页面。
上面的实例中使用了一些 Flask-Login 的基本特性,Flask-Login 还提供了一些其他重要特性
记住我
记住我,并不是用户登出之后,再次登录时自动填写用户名和密码(这是浏览器的功能),而是在用户意外退出后(比如关闭浏览器)不用再次登录。
如果用户本地的 cookie 失效了,Flask-Login 会自动将用户 Session 放入 cookie 中。
开启方法是将 login_user 方法的命名参数 remember 设置为 True,此功能默认是关闭的
Session 防护
Session 信息一般存放在 cookie 中,但是 cookie 不够安全,容易被窃取其中 Session 信息,伪造用户登录系统,幸运的是 Flask-Login 提供了 Session 防护机制,提供有 basic 和 strong 两种保护等级,通过 login_manager.session_protection 来开关和设置等级,默认等级为 basic,如果设置为 None 将关闭 Session 防护机制。
在保护机制开启的情况下,每次请求会根据用户的特征(一般指有用户IP、浏览器类型生成的哈希码)与 Session 中的对比,如果无法匹配则要求用户重新登录,在强模式下( strong )一旦匹配失败会删除登录者 Session,以消除攻击者重构 cookie 的可能
Request Loader
有时候因为一些原因不想或者无法使用 cookie,可以将 Session 记录在其他地方,比如 Header 中或者请求参数中,那么构造用户 Session 时就需要将 user_loader 替换为 request_loader, request_loader 将 request 作为参数,这样就可以从请求的任何数据中获取 Session 信息了
现在单页 Web 项目很流行,使用各种 Js 框架,通过 Ajax 和服务器的 Api 进行交互,实现类似原生 app 效果,很酷,对 Flask 来说小菜一碟,是时候了解下 Flask-RESTful 了,官方文档:快速入门 — Flask-RESTful 0.3.1 documentation
开始前先了解下 RESTful,阮一峰有这样的解释:
网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备……)。 因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现”API First”的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论
也就是说 RESTful 一个框架和互联网应用的设计原则,遵循这个设计原则,可以让应用脱离前台展现的束缚,支持不同的前端设备。
Flask 的 RESTful 模块是 flask-restful ,使用 pip 安装:
pip install flask-restful
如果安装顺利,可以在 Python Shell 环境下导入
from flask_restful import Api
安装好后,简单试试。 flask-restful 像之前的 bootstrop-flask 以及 flask-sqlalchamy 模块一样,使用前需要对 Flask 应用进行初始化,然后会得到当前应用的 api 对象,用 api 对象进行资源绑定和路由设置:
- from flask import Flask
- from flask_restful import Api, Resource
-
- app = Flask(__name__)
-
- api = Api(app) # 初始化得到 api 对象
-
- #上面代码中从 flask_restful 中引入的 Resource 类是用来定义资源的,具体资源必须是 Resource 的子类,下面定义一个 HelloRESTful 资源:
- class HelloRESTful(Resource):
- def get(self):
- return {'greet': 'Hello Flask RESTful!'}
-
- def post(self):
- data = json.loads(request.get_data())
- name = data.get('data')
- return name
-
- #给资源绑定 URI:
- api.add_resource(HelloRESTful, '/')
-
- if __name__ == '__main__': # 别忘了启动应用的代码
- app.run(debug=True)

在终端或者命令行下运行 python app.py 启动应用,访问 127.0.0.1:5000 查看效果,将会看到 JSON 格式的数据输出:
- {
- "greet": "Hello Flask RESTful!"
- }
也可以用 curl 工具在终端或者命令行下发送请求:
- # -s 开启安静模式
- curl http://localhost:5000 -s
-
- curl -H "Content-Type:application/json" -X POST -d '{"data":12}' http://127.0.0.1:8000/TestApi
- {
- 12
- }
从上面代码中可以看到,资源是 Resource 类的子类,以请求方法( GET、POST 等)名称的小写形式定义的方法,能对对应方法的请求作出相应,例如上面资源类中定义的 get 方法可以对 GET 请求作出相应,还可以定义 put、post、delete 等,称之为视图方法。
例如创建一个 todo 字样,支持获取代办事项和新增代办事项:
- # 初始化待办列表
- todos = {
- 'todo_1': "读《程序员的自我修养》",
- 'todo_2': "买点吃的",
- 'todo_3': "去看星星"
- }
- class Todo(Resource):
- # 根据 todo_id 获取代办事项
- def get(self, todo_id):
- return { todo_id: todos[todo_id] }
-
- # 新增一个待办事项
- def put(self, todo_id):
- todos[todo_id] = request.form['data']
- return {todo_id: todos[todo_id]}
为 Todo 资源指定 URI:
api.add_resource(Todo, '/todo/<string:todo_id>/')
启动项目,用 curl 工具测试:
- # 读取 key 为 todo_1 的待办事项
- curl http://localhost:5000/todo/todo_1/
- {
- "todo_1": "\u8bfb\u300a\u7a0b\u5e8f\u5458\u7684\u81ea\u6211\u4fee\u517b\u300b"
- }
-
- # 创建一个 key 为 todo_4 的代办事项
- curl http://localhost:5000/todo/todo_4/ -d "data=学习 Flask" -X PUT
-
- {
- "todo_4": "\u5b66\u4e60 Flask"
- }
Flask-RESTful 支持多种视图方法的返回值:
- class Todo1(Resource):
- def get(self):
- # 直接返回
- return { 'task': 'Hello world'}
-
- class Todo2(Resource):
- def get(self):
- # 返回内容及状态码
- return {'task': 'Hello world'}, 201
-
- class Todo3(Resource):
- def get(self):
- # 返回内容,状态码以及 Header
- return {'task': 'Hello world'}, 200, {'Etag': 'some-opaque-string'}
为三个资源指定 URI:
- api.add_resource(Todo1, '/todo_1/')
- api.add_resource(Todo1, '/todo_2/')
- api.add_resource(Todo1, '/todo_3/')
启动项目后,用 curl 工具来测试:
- curl http://localhost:5000/todo_1/
- {
- "task": "Hello world"
- }
-
- # -请求 todo_2 并显示出 HTTP 标头,HTTP 状态码为 201
- curl http://localhost:5000/todo_2/ -i
- HTTP/1.0 201 CREATED
- Content-Type: application/json
- Content-Length: 30
- Server: Werkzeug/0.16.0 Python/3.7.5rc1
- Date: Thu, 31 Oct 2019 14:12:54 GMT
-
- {
- "task": "Hello world"
- }
-
- # -请求 todo_3 并显示出 HTTP 标头,HTTP 状态码为 200 ,标头中还有 Etag
- curl http://localhost:5000/todo_3/ -i
- HTTP/1.0 200 OK
- Content-Type: application/json
- Content-Length: 30
- Etag: some-opaque-string
- Server: Werkzeug/0.16.0 Python/3.7.5rc1
- Date: Thu, 31 Oct 2019 14:14:57 GMT
-
- {
- "task": "Hello world"
- }

从上面可以看到,通过 api.add_resource 方法来为资源设置路由。第一个参数是资源类,第二个参数是路由,和之前介绍的 @app.route 注解参数一样
可以为一个资源制定多个路由,例如:
api.add_resource(Todo, '/todo/', '/mytodo/')
http://localhost:5000/todo/ 和 http://localhost:5000/mytodo/ 都将指向 Todo
既然路由,就应该有 endpoint,通过命名参数 endpoint 指定:
api.add_resource(Todo, '/todo/', endpoint='todo_ep')
设置路由的 endpoint 为 todo_ep,如果不指定,endpoint 就是资源类名的小写形式
endpoint 是 Flask 中对具体路由的内部的具体定义,一般作为 url_for 方法的第一个参数,即通过 endpoint 获得该路由的 URL,在列出 RESTful 资源 URL 时非常有用。
RESTful 服务器对请求数据有很强的依赖,就请求数据的获取及校验是很繁琐的事情,还好 Flask-RESTful 提供了非常好的请求解析工具 reqparse,不仅可以获取请求数据,还可以对数据进行校验并返回合适的错误消息。
- from flask_restful import reqparse # 引入 reqparse 模块
- # ...
-
- parser = reqparse.RequestParser() # 定义全局的解析实体
- # 定义参数 id,类型必须是整数
- parser.add_argument('id', type=int, help='必须提供参数id且类型正确')
- # 定义参数 name,且为必填
- parser.add_argument('name', required=True)
- # ...
-
- class Reqparser(Resource):
- def get(self):
- args = parser.parse_args() # 获取解析器中定义的参数 并校验
- return args
-
- api.add_resource(Reqparser, '/reqparser/') # 指定路由

看下效果:
- # 提供一个非整数参数 id
- curl http://localhost:5000/reqparser/ -d "id=noint" -X GET
- {
- "message": {
- "id": "必须提供参数 id且类型正确"
- }
- }
-
- # 不提供参数 name
- curl http://localhost:5000/reqparser/
- {
- "message": {
- "name": "Missing required parameter in the JSON body or the post body or the query string"
- }
- }
- #必须提供两个参数,且类型正确
- http://127.0.0.1:5000/reqparser?name=Ads&id=123
- {
- "id": 123,
- "name": "Ads"
- }

parser = reqparse.RequestParser(bundle_errors=True),或者设置应用配置,如 app.config['BUNDLE_ERRORS'] = Trueparser.add_argument('id', type=int, help='必须提供参数 id', location='args')请求解析器支持继承,可以定义最高级别的解析器,逐渐细化,最后应用的具体资源上:
- from flask_restful import reqparse
-
- parser = reqparse.RequestParser()
- parser.add_argument('foo', type=int)
-
- parser_copy = parser.copy() # 继承
- parser_copy.add_argument('bar', type=int) # parser_copy 将有两个参数
-
- # 改变继承来的参数 foo 必填且的获取位置为 querystring
- parser_copy.replace_argument('foo', required=True, location='args')
-
- # 删除继承来的参数 foo
- parser_copy.remove_argument('foo')
请求解析处理用收到的信息,对于输入的信息也可以处理,通过 Flask-RESTful 提供的类 fields 和注解 marshal_with 来实现:
- from flask_restful import Resource, fields, marshal_with
-
- resource_fields = {
- 'name': fields.String,
- 'address': fields.String,
- 'date_updated': fields.DateTime(dt_format='rfc822'),
- }
-
- class TodoFormat(Resource):
- @marshal_with(resource_fields, envelope='resource')
- def get(self):
- return db_get_todo() # 某个获得待办事项的方法
marshal_with 注解,指定格式化模板,和封装属性名格式化模板属性名,需要在响应函数返回的对象属性中匹配,如果需要会要对字段重命名,可以这样:
- fields = {
- # name 将被重命名为 private_name
- 'name': fields.String(attribute='private_name'),
- 'address': fields.String
- }
返回值中没有可以定义默认值:
- fields = {
- # 为 name 设置默认值
- 'name': fields.String(default='Anonymous User'),
- 'address': fields.String
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。