Leetao's Blog

Talk is cheap, show me the code

0%

最近一直在看《HTTP The Definitive Guide》,对此大家可能有点陌生,但是一提它的中文名称那就是耳熟能详了,它的中文书名是《http权威指南》.在看到HTTP Messages这一章Status Code这一节,发现状态码302、303、307这三个很相似,然后对此进行了对比,特此记录.

302 Found

This is an example of industry practice contradicting the standard. The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect (the original describing phrase was “Moved Temporarily”, but popular browsers implemented 302 with the functionality of a 303 See Other.Therefore, HTTP/1.1 added status codes 303 and 307 to distinguish between the two behaviours. However, some Web applications and frameworks use the 302 status code as if it were the 303 ——[1]

在看下维基百科的定义:

The HTTP response status code 302 Found is a common way of performing URL redirection.
An HTTP response with this status code will additionally provide a URL in the header field location. The user agent (e.g. a web browser) is invited by a response with this code to make a second, otherwise identical, request to the new URL specified in the location field. The HTTP/1.0 specification (RFC 1945) initially defined this code, and gives it the description phrase “Moved Temporarily”.
Many web browsers implemented this code in a manner that violated this standard, changing the request type of the new request to GET, regardless of the type employed in the original request (e.g. POST).For this reason, HTTP/1.1 (RFC 2616) added the new status codes 303 and 307 to disambiguate between the two behaviours, with 303 mandating the change of request type to GET, and 307 preserving the request type as originally sent. Despite the greater clarity provided by this disambiguation, the 302 code is still employed in web frameworks to preserve compatibility with browsers that do not implement the HTTP/1.1 specification.
As a consequence, the update of RFC 2616 changes the definition to allow user agents to rewrite POST to GET ——[2]

结合上面两个我们大致可以总结出这样一些结论:

在 RFC 1945 中初次定义状态码302,并将其描述位”Moved Temporarily”(暂时移动).预期关于302的使用应该如下,当一个http的response状态码为302的时候,其响应头肯定回包含一个可选的URL(如下图).客户端预期需要根据该URL进行二次请求.正常情况下客户端的请求方式应该与之前保持一致,但是许多Web浏览器并没有遵守这个规定,而是不管原始请求使用何种类型,都将新请求的请求类型变为GET.由于这个原因,在HTTP/1.1 (RFC 2616)添加 303 和 307 这两个状态码,其中 303强制将请求类型改变为GET,307保留原始发送的请求类型. 而302则用来维持与尚未实现HTTP/1.1规范的浏览器的兼容性.

303 See Other (since HTTP/1.1)

The response to the request can be found under another URI using the GET method. When received in response to a POST (or PUT/DELETE), the client should presume that the server has received the data and should issue a new GET request to the given URI.

对于303我们从上面就可以得知,二次请求的方式一定为GET,如果第一次请求为其他类型的话,我们默认认为它已经接受到了数据.该状态码可以用来相应任何方式的HTTP请求.

它主要用于允许POST操作的输出将客户端重定向到选定的资源,因为这样做提供了与POST响应相对应的信息,并且该表单可以独立于原始请求单独标识、书签和缓存,个人理解是正常情况下我们在一个页面进行了post操作,然后得到服务器响应在当前页面获取到相应数据,显然这种情况下页面是无法保存书签和缓存的,一旦我们重新访问这个页面,之前的数据还是需要我们重新请求的,303则是将响应的内容重新定向到一个新的页面,并且下次访问该页面的时候响应的数据还是仍然存在的,比如消息确认页面或上传进度页面.

对于303响应GET方式的请求,我个人是有点困惑的.RFC7321表述内容如下:

A 303 response to a GET request indicates that the origin server does not have a representation of the target resource that can be transferred by the server over HTTP. However, the Location field value refers to a resource that is descriptive of the target resource, such that making a retrieval request on that other resource might result in a representation that is useful to recipients without implying that it represents the original target resource. Note that answers to the questions of what can be represented, what representations are adequate, and what might be a useful description are outside the scope of HTTP. ——[3]

直观的翻译大意如下:
对GET请求的303响应表明,源服务器不具有可由服务器通过HTTP传输的目标资源的表示形式。但是,Location字段值是指描述目标资源的资源,因此,对该其他资源发出检索请求可能会导致对收件人有用的表示,而不会暗示它代表原始目标资源。

我的理解是对于GET方式返回303代表,其目标资源并不能很好的借由服务器通过HTTP协议很好的展示出来.响应头中的Location的值指的是一种资源,用来是描述目标资源的.对该资源请求可能获取到对客户端有用的信息.

307 Temporary Redirect (since HTTP/1.1)

In this case, the request should be repeated with another URI; however, future requests should still use the original URI. In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. For example, a POST request should be repeated using another POST request

307就相对毕竟简单些,如果返回的状态码为307,则表明使用head中location的URI进行再一次的请求,但是以后的请求仍然使用原来的URI.与以往实现302的方式不同,在重新发出原始请求时,不允许更改请求方法。例如,应该使用另一个POST请求重复POST请求.

参考链接

[1] 3xx_Redirection

[2] HTTP_302

[3] 303 See Other

问题

这几天用go写了一个api接口,用postman测试的时候,数据返回都正常的,然后用 axios 请求的时候,服务器始终接收不到前台发来的的参数,感觉问题可能出现在请求头,然后打开chrome开发者工具,查看一下请求详情

axios 请求的详情

Request-Headers 的 Content-Typeapplication/json;charset=UTF-8,Request Payload格式为

{param1: “value1”, params: “value2”}

postman 请求的详情

Request-Headers 的 Content-Typeapplication/x-www-form-urlencoded,URL encode 为

param1=value1&param2=value2

对比了一下很明显问题就出在这里,接下来该怎么解决问题

问题的解决

使用 URLSearchParams

此方法不推荐,兼容性不太好,见下图:

使用示例代码:

1
2
3
4
var params = new URLSearchParams();
params.append('param1', 'value1');
params.append('param2', 'value2');
axios.post('/foo', params);

使用 qs 库

使用示例代码:

1
2
var qs = require('qs');
axios.post('/foo', qs.stringify({ 'bar': 123 }));

安装 git for windows

想要使用 go get.需要先安装好 git

设置 GOPATH

使用go get安装的相关包将会保存在改目录下,为了使得安装的相关包立马生效,可以事先在 GOPATH 所在目录新建三个文件,分别为 bin,pkg,src

GOPATHGOROOT 路径不能相同

两者路径相同的话,使用go get 安装会提示相关错误信息

Q&A

Q1. 用IntegerField来限制form只能用数字 但是发现它只能接受1以上 不能接受0 如何设置

A1:添加代码validators=[InputRequired()]

Q2. 让 IntegerField 的输入框只允许输入数字,即选渲染后的输入框类型为 number

A2: 添加代码widget=NumberInput(), 引入 from wtforms.widgets.html5 import NumberInput,默认的 widget 为 TextInput(),见链接[1]

Q3. 如何在一个页面中使用两个Form

A3: 确保两个form中的字段不相同,因为渲染之后字段名即为 id,判断语句应如下,其他方法可参考[2]:

1
2
3
4
if form1.validate_on_sumite() and form1.submite1.data():
...
elif form2.validate_on_submit() and form2.submit2.date():
...

Q4. 添加 placeholder

A4: 添加代码 render_kw={'placeholder': "placeholder"}

参考

[1]wtforms-fields-core.py
[2]Multiple forms in a single page using flask and WTForms
[3]widgets
[4]fields

在windows下装个docker真是不容易,到最后一步,没有办法开启VTx. 其实很简单,前提如果是可以进入bios的情况下. 不知道怎么会是我的台式机(HP EliteDesk 800 G3 SFF )始终无法进入 bios,开机按 F10 会有声音但是没有任何画面,显示屏没有任何画面,我以为是连接显示器的接口问题,从集显接口换了蓝色的独显接口,但是还是没有任何作用.至到我找到了 BiosConfigUtility 这个 HP 官网的软件.从官网下载软件并安装.安装完成之后进入安装完成界面,如下图:

然后打开命令行以管理员的身份运行,切换到该目录下,执行下述命令:

你会发现在当前目录下你多了一个 “config64.xml” 的文件,如果你是 32 位的系统,使用不带有 64 后缀的命令,然后使用编辑器打开该 xml,注意以管理员的身份打开该编辑器,否则待会你无法保存这个文件,当然如果你将这个文件保存到非系统盘路径下则不需要.然后找到有关虚拟支持的设置:

编辑完成保存之后,在执行如下命令:

1
BiosConfigUtility64 /set:"config64.xml"

如果控制台输出内容大意为没有错误发生,则说明你设置成功了!然后重启,docker 终于可以正常运行了!!!

定义

NotImplemented 的在标准库的定义如下:

Special value which should be returned by the binary special methods (e.g. eq() , lt(), add(), rsub(), etc.) to indicate that the operation is not implemented with respect to the other type; may be returned by the in-place binary special methods (e.g. imul(), iand(), etc.) for the same purpose. Its truth value is true.

翻译成中文的大概意思:NotImplemented 是一个可由内置函数(eq() , lt(), add(), rsub() 等等)返回的特殊值,用以表明该操作没有对另一种类型的实现.可能会被其他的特殊方法( imul(), iand() 等等)就近处理用以同样的目的,从而返回.它的实际值为True.最后一句话的真是含义如下图:

与 False,True,None 这几个内置常量不同的是 NotImplemented 对它赋值,并且不会产生 SyntaxError. 如下图:

Notimplemented 和 Notimplementederror

在关于 NotImplemented 的定义下面,官方还特意加了一条提示:

NotImplementedError and NotImplemented are not interchangeable, even though they have similar names and purposes.

NotImplementedError

官方定义如下:

This exception is derived from RuntimeError. In user defined base classes, abstract methods should raise this exception when they require derived classes to override the method, or while the class is being developed to indicate that the real implementation still needs to be added.

翻译过来的意思是说:此异常从 RuntimeError 派生出来的.在用户定义的基类中,当抽象方法需要派生类覆盖方法时,或者在开发该类以指示仍然需要添加实际实现时,抽象方法应该引发此异常.

同样有一条提示:

It should not be used to indicate that an operator or method is not meant to be supported at all – in that case either leave the operator / method undefined or, if a subclass, set it to None.

大概意思是指 NotImplementedError 根本不应该被用来表示一个操作符或者方法不被支持的情况(对于一下 NotImplemented,but why?just see next).在这种情况下,要么保留操作符/方法未定义,要么将子类设置为无.

Why return NotImplemented instead of raising NotImplementedError

前者是常量后者是异常,但是为什么在操作没有实现的时候,返回前者而不是后者呢?

在 Python 中对列表进行排序时,会经常间接使用类似 lt() 这类比较运算的方法.有时 Python 的内部算法会选择别的方法来确定比较结果,或者直接选择一个默认的结果。如果抛出一个异常,则会打破排序运算,因此如果使用 NotImplemented 则不会抛出异常,这样 Python 可以尝试别的方法。

NotImplemented 在运行时发出信号,要求其他人满足操作. 在表达式 a==b 中,如果 a.eq(B)返回 NotImplented,那么 Python 将尝试 b.eq(A).如果b知道的足够多,可以返回 truefalse,那么表达式就可以成功. 如果没有,那么运行时将返回到内置行为(基于 ==!=的标识.

使用

我们通过实现上面提到的 eq() 来实现上面提到的例子,来加深印象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# example.py

class A(object):
def __init__(self, value):
self.value = value

def __eq__(self, other):
if isinstance(other, A):
print('Comparing an A with an A')
return other.value == self.value
if isinstance(other, B):
print('Comparing an A with a B')
return other.value == self.value
print('Could not compare A with the other class')
return NotImplemented

class B(object):
def __init__(self, value):
self.value = value

def __eq__(self, other):
if isinstance(other, B):
print('Comparing a B with another B')
return other.value == self.value
print('Could not compare B with the other class')
return NotImplemented

然后运行结果如下图:

对于 a == ab == b ,显然结果如我们预期那样,因为我们实现已经定义好了自己比较自己的方法.重点在 b == a,我们并没有在 B 中定义 B 和 其他类比较的方法,因此打印出了 “Could not compare B with the other class”,然后程序接下来去调用了 a.eq(B),一切如我们之前所说的那样.

参考

NotImplemented

exception NotImplementedError

Why return NotImplemented instead of raising NotImplementedError

Python中内置的NotImplemented类型

最近闲来无事折腾了一下自己的个人公共号,通过 Flask 构建了几个简单的小功能然后部署到服务器上.网上关于部署的教程有很多,这里我就简单介绍一下我的部署步骤.

部署的准备工作

更新并安装下述的包

1
2
apt-get update
apt-get install python-pip python-dev nginx

创建虚拟环境

切到项目所以目录下,由于我使用的是 python 版本是 3.5.2 的,所以通过下述命令去创建虚拟环境

1
python -m venv Env # Env 为创建的虚拟环境的名称

然后激活环境安装相应的库

1
2
source Env/bin/activate
pip install gunicorn # 这里省略了其他需要安装的包

创建 WSGI 文件

通过命令行在项目目录下创建 wsgi.py 文件

1
nano wsgi.py # 原来用 vim,后来发现用 nano 也很爽

这个文件内容其实很简单就是让程序运行起来:

1
2
3
4
5
6
7
# wsgi.py
from httpbin import create_app # httpbin 项目目录

application = create_app('default')

if __name__ == '__main__':
application.run()

然后保存

使用 Gunicorn 运行应用

由于服务默认开发端口仅有80和443端口,对此我也懒得修改,直接将服务挂载到80端口进行测试

1
gunicorn --bind 0.0.0.0:80 wsgi

通过访问我应用的指定的api然后成功获得了数据,然后开始进行下一步

创建一个启动脚本

这个脚本顾名思义允许 Ubuntu 自动启动 Gunicorn 从而挂载我的 Flask 应用.在 /etc/init 目录下创建以 .conf 结尾的文件:

1
nano /etc/init/wechat.conf

文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# wechat.conf

description "Gunicorn application server running wechat"

start on runlevel [2345]
stop on runlevel [!2345]

respawn
setuid root # 我的是root用户,如果你是在其他用户下运行则将其他改为其他用户名称
setgid www-data

env PATH=/home/py/wechat/Env/bin # python 环境目录
chdir /home/py/wechat
exec gunicorn --workers 3 --bind unix:wechat.sock -m 007 wsgi

保存退出,然后尝试运行你的脚本文件

1
start wechat

想要查看是否运行成功的话,可以使用下述命令:

1
ps -aux | grep wechat

如果你在输出的结果中看到相应的进程了就说明你的脚本运行成功了.然后就可以进行下一步

配置 Nginx

在这里介绍了如何简单配置 Nginx,但是在实际应用中需要思考一下是否真的需要用到 Nginx,Nginx有很多功能,但是对于像这样简单的小应用,直接 Gunicorn 裸跑也是没毛病的,但是你说你一台服务器挂多个域名,避免 Gunicorn 独占 80 端口,用 Nginx 这个更没毛病.

/etc/nginx/sites-available 目录下创建项目配置文件 wechat,没必要将缺省文件删除

1
nano /etc/nginx/sites-available/wechat

配置文件内容如下:

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name server_domain_or_IP;

location / {
include proxy_params;
proxy_pass http://unix:/home/py/wechat/wechat.sock;
}
}

保存退出,然后为了确保配置文件生效,将其链接到 sites-enabled 目录下:

1
ln -s /etc/nginx/sites-available/wechat /etc/nginx/sites-enabled

测试一下配置文件是否有语法错误:

1
nginx -t

如果看见 succssful 的成功单词就说明配置文件没有任何问题,然后重启 nginx

1
service nginx restart 

大功告成~

前段时间在讲述 Flask-Login 的源码中遇见了一个函数 ——partial,这个函数来自于functools模块,这个模块定义为”The functools module is for higher-order functions: functions that act on or return other functions. In general, any callable object can be treated as a function for the purposes of this module”,翻译成中文的大概意思就是”functools 用于高阶函数:指那些作用于函数或者返回其他函数的函数。总的来说,这个模块的目标就是将任何可调用的对象都可以被视为函数。”

functools.partial

返回一个新的部分对象,当被调用的时候,相当于原有的对象携带了默认的参数.如果在调用过程中传递了更多的参数,将会被追加到args.如果提供了其他的关键字参数,则会覆盖或者扩展关键字:

1
2
3
4
5
6
7
8
9
def partial(func, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = keywords.copy()
newkeywords.update(fkeywords)
return func(*(args + fargs), **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc

当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单.

eg:

int()函数,可以将字符串转为整数,简单看一下它的函数使用说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class int(object)
| int(x=0) -> integer
| int(x, base=10) -> integer
|
| Convert a number or string to an integer, or return 0 if no arguments
| are given. If x is a number, return x.__int__(). For floating point
| numbers, this truncates towards zero.
|
| If x is not a number or if base is given, then x must be a string,
| bytes, or bytearray instance representing an integer literal in the
| given base. The literal can be preceded by '+' or '-' and be surrounded
| by whitespace. The base defaults to 10. Valid bases are 0 and 2-36.
| Base 0 means to interpret the base from the string as an integer literal.
| >>> int('0b100', base=0)
| 4

从上述说明中我们可以看见,int还有一个额外的参数base,base 默认值为10,也就意味着默认情况下将字符串转化为十进制的数.如果我们需要转化成二进制怎么办?在不使用其他模块的情况下,只需要int(x,base=2),那么如果是借助partial,该如何使用呢?

1
2
3
4
5
6
7
8
9
10
>>>from functools import partial
>>>int2 = partial(int,base=2) # 这里是传递了关键字参数,即keywords
>>>int2('1000')
>>>8
>>>str_3_int = partial(int,'3') # 传递了参数args
>>>str_3_int()
>>>3 # 不够清楚的话可以接着看下面的例子
>>>max_with_10 = partial(max,10)
>>>max_with_10(2,3,4)
>>>10

想必大家对 Flask-Login 这个 Flask 扩展肯定不会陌生,毕竟作为一个应用,用户登录之后,他们的认证状态是需要被记录下来的,浏览其他的页面也是需要使用这个状态的,但是这一过程是怎么发挥作用的呢?让我们从源码的层次上简单认识一下.

Flask-Login 使用的准备

使用 flask-login 的之前,我们必须要实现下述四个方法:

但是为什么要实现这个方法呢?别着急我们在下面会释疑.
除此之外我们还需要写下述的这样的代码:

1
2
3
4
5
6
@login_manager.user_loader
def load_user(username):
if query_user(username) is not None:
curr_user = User()
curr_user.id = username
return curr_user

上面的为用户的回调函数接收字符串表示的唯一用户标识符,如果能找到该用户,则返回该用户对象否则返回None.但是user_loader究竟是什么?
源码:

1
2
3
4
5
class LoginManager(object):
...
def user_loader(self, callback):
self.user_callback = callback
return callback

似乎没什么作用呢?别着急,接着往下看,我们通过具体使用去理解源码

login_user()

但凡时候 Flask-Login 应该都用过 login_user(),所以让我们看看使用这个函数发生了什么.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def login_user(user, remember=False, force=False, fresh=True):
if not force and not user.is_active: # 最初必须实现的四个方法之一,在这里发挥作用,如果该用户不允许登录则直接返回False(黑名单etc)
return False

user_id = user.get_id() # 修改
session['user_id'] = user_id
session['_fresh'] = fresh
session['_id'] = current_app.login_manager._session_identifier_generator() # 根据客户端的User Agent和IP生成标识,strong模式下一旦用户标识检测失败,便会清空所有的session内容

if remember:
session['remember'] = 'set'

_request_ctx_stack.top.user = user
user_logged_in.send(current_app._get_current_object(), user=_get_user())
return True

主要内容是将用户信息放入session中,并为改session生成标识符.

@login_required

我们通过 @login_required 去限制用户页面访问,这一过程是怎么实现的?

1
2
3
4
5
6
7
8
9
10
11
def login_required(func):
@wraps(func)
def decorated_view(*args, **kwargs):
if request.method in EXEMPT_METHODS: // EXEMPT_METHODS = set(['OPTIONS']) config.py 第 49 行,通常该请求是获取服务器支持的HTTP请求方法
return func(*args, **kwargs)
elif current_app.login_manager._login_disabled: # 如果 _login_disabled 被设置为 True,则装饰器将会被忽略,通常用在单元测试可以很方便的关闭认证
return func(*args, **kwargs)
elif not current_user.is_authenticated: # 判断用户是否登录
return current_app.login_manager.unauthorized() # 没有登录的情况下:1.如果注册了 LoginManager.unauthorized_handler 则这个时候调用这个函数 2. 向用户提示 LoginManager.login_message信息 3.有 login_view的情况下,跳转到login_view,没有则返回abort(401)
return func(*args, **kwargs)
return decorated_view

unauthorized()

在上述@login.required中我们说明了,unauthorized()在需要登录的情况下采取的行动,现在简单的看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def unauthorized(self):
user_unauthorized.send(current_app._get_current_object())

if self.unauthorized_callback:
return self.unauthorized_callback()

if request.blueprint in self.blueprint_login_views:
login_view = self.blueprint_login_views[request.blueprint]
else:
login_view = self.login_view

if not login_view:
abort(401)

if self.login_message:
if self.localize_callback is not None:
flash(self.localize_callback(self.login_message),
category=self.login_message_category)
else:
flash(self.login_message, category=self.login_message_category)

config = current_app.config
if config.get('USE_SESSION_FOR_NEXT', USE_SESSION_FOR_NEXT):
login_url = expand_login_view(login_view)
session['next'] = make_next_param(login_url, request.url)
redirect_url = make_login_url(login_view)
else:
redirect_url = make_login_url(login_view, next_url=request.url)

return redirect(redirect_url)

current_user

在登录之后,我们在需要使用当前登录对象的时候,都会使用 current_user,那么它是怎么发挥作用的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
current_user = LocalProxy(lambda: _get_user()) // 当前用户的代理,这里重点关注 _get_user()

# _get_user()
# 调用 login_user() 之后显然 _request_ctx_stack.top 中存在 user,则直接返回
def _get_user():
if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'):
current_app.login_manager._load_user()

return getattr(_request_ctx_stack.top, 'user', None)

# 那么如果不存在,会发生什么? 调用 _loder_user()

# _loader_user()
def _load_user(self):
user_accessed.send(current_app._get_current_object())

config = current_app.config
if config.get('SESSION_PROTECTION', self.session_protection): // session 保护,可以取值为 None(禁用),basic(在 basic 模式下或会话是永久的,如果该标识未匹配,会话会简单地被标记为非活 跃的,且任何需要活跃登入的东西会强制用户重新验证,前提你已经使用了活跃登入机制),strong(在 strong 模式下的非永久会话,如果该标识未匹配,整个会话或者记住的令牌如果存在将会被删除)
deleted = self._session_protection()
if deleted:
return self.reload_user()

# 如果 记住你的 cookie 存在,而 session 不存在,则会将 cookie 中的 user_id 赋值给 session
# 但是如果这个时候处于登出的状态的话,会发生什么呢?
is_missing_user_id = 'user_id' not in session
if is_missing_user_id:
cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
header_name = config.get('AUTH_HEADER_NAME', AUTH_HEADER_NAME)
has_cookie = (cookie_name in request.cookies and
session.get('remember') != 'clear') // 登出的时候 has_cookie = False
if has_cookie:
return self._load_from_cookie(request.cookies[cookie_name])
elif self.request_callback: //定义在 request_loader() 中, 正常情况下我们并不使用,仅当我们不想使用 cookie 的情况下登录用户才会考虑 request_loader 回调.
return self._load_from_request(request)
elif header_name in request.headers:
return self._load_from_header(request.headers[header_name]) # 该函数调用链中的 header_loader() 已经被弃用

return self.reload_user()

logout_user()

登录要验证账户密码什么的,显的比较复杂,但是登出就很简答了,你不需要传递任何参数,只需调用这个函数,那让我们看看这个函数发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def logout_user():
user = _get_user() # 从_request_ctx_stack.top.user,session 或者 remember_me cookie 中获取

if 'user_id' in session:
session.pop('user_id')

if '_fresh' in session:
session.pop('_fresh')

cookie_name = current_app.config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
if cookie_name in request.cookies:
session['remember'] = 'clear'

user_logged_out.send(current_app._get_current_object(), user=user)

current_app.login_manager.reload_user()
return True

该函数将session中的user相关信息全部清空,然后重新加载用户,正常逻辑下由于重新加载用户后如果当前页面需要登录才能访问的话,则会跳转到登录视图.

fresh_login_required

当用户登入,他们的会话被标记成“新鲜的”,就是说在这个会话只中用户实际上登录过。当会话销毁用户使用“记住我”的 cookie 重新登入,会话被标记成“非新鲜的”。fresh_login_required 除了验证用户登录,也将确保他们的登录是“新鲜的”。如果不是“新鲜的”,它会把用户送到可以重输入验证条件的页面,主要用户修改个人信息的敏感操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
def fresh_login_required(func):
@wraps(func)
def decorated_view(*args, **kwargs):
if request.method in EXEMPT_METHODS:
return func(*args, **kwargs)
elif current_app.login_manager._login_disabled:
return func(*args, **kwargs)
elif not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
elif not login_fresh(): // 获取session 中的 refresh 值
return current_app.login_manager.needs_refresh() // 1.出现 LoginMange.needs_refress_message 2.跳转到 LoginManger.refresh_view,如果没有设置则会提示 401 错误
return func(*args, **kwargs)
return decorated_view

confirm_login

将会话重新标记为”新鲜”

1
2
3
4
def confirm_login():
session['_fresh'] = True
session['_id'] = current_app.login_manager._session_identifier_generator() // 重新生成会话id
user_login_confirmed.send(current_app._get_current_object())

reload_user()

接着 logout_user() 中调用 reload_user().很显然这个函数的功能就是重新加载用户,具体实现让我们看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# logout_user() 中调用, user 使用默认参数为 None
def reload_user(self, user=None):
ctx = _request_ctx_stack.top // _request_ctx_stack 一个保存对象的栈,可以返回看 login_user() 源码的倒数第三行,登录的时候将 user 保存在 _request_ctx_stack.top.user 中

if user is None:
user_id = session.get('user_id') // logout_user() 中 session 已经将 user_id 移除
if user_id is None:
ctx.user = self.anonymous_user() //见下文
else:
if self.user_callback is None: //这里解释了为什么我们需要定义 @login_manager.user_loader
raise Exception(
"No user_loader has been installed for this "
"LoginManager. Add one with the "
"'LoginManager.user_loader' decorator.")
user = self.user_callback(user_id) // 这里我们最初定义的user_loader发挥作用,通过调用self.user_callback()获取用户
if user is None:
ctx.user = self.anonymous_user()
else:
ctx.user = user
else:
ctx.user = user

anoymous_user

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.anonymous_user = AnonymousUserMixin // 匿名类

# AnonymousUserMixin
class AnonymousUserMixin(object):
@property
def is_authenticated(self):
return False

@property
def is_active(self):
return False

@property
def is_anonymous(self):
return True

def get_id(self):
return

原文:https://blog.miguelgrinberg.com/post/migrating-from-flask-script-to-the-new-flask-cli

在Falsk 0.11版本, Flask 基于 Click 引进了一个新的命令行功能,该功能包含了 flask命令. 在此之前 Flask 并没有提供任何关于构建命令行接口(CLIs)的支持,但是 Flask=Script作为三方拓展提供了类似的功能.

在 Flask CLI发布超过一年的时间里,我仍然看见有许多项目基于 Flask-Script. 我猜测这是由于没有特别重要的原因促使人们迁移到 Flask CLI中,毕竟 Flask-Script 运转地很好,至少没什么毛病. 但是实际上,自从 2014 年开始 Flask-Script 就再也没有过官方发布版本并且看起来也没有再维护了.在本文中,我想向你展示如何将 Flasky 应用程序从 Flask-Script 迁移到 Click ,以便你可以了解两者的不同之处,并决定是否迁移应用程序.

Flask-Script 有什么问题?

在我看来,Flask-Script 有个严重的缺陷.但有趣的是,这是一个由Flask reloader 中的老的设计问题直接引起的问题,如果你使用app.run()启动应用程序,而不使用Flask-Script,仍然会出现这个问题.

如果你在调试模式下启动应用程序,则会运行两个 Flask 进程. 第一个进程用来观察源文件的更改,第二个是实际的 Flask 服务器. 当任何源文件更改时,监视进程将杀死服务器进程,然后启动另一个进程,从而使用新的源文件.

当你无意中在某个源文件中引入语法错误时,会出现一个问题. 这个时候观察者进程并不知道,所以它仍然会杀死旧的服务器,并启动一个新的. 但是新服务器不会启动,因为 Python 在解析修改后的文件时会引发错误.当服务器进程在错误中退出时,观察者进程也会放弃并退出,从而迫使你在修复代码中的错误后重新启动整个进程.

当您使用新的 Flask Run 命令启动重新加载程序时,效果会好得多.在发送第一个请求之前,不会导入应用程序,此时,如果在导入源文件时发生错误,就会像处理运行时发生的错误一样进行处理.这意味着,如果启用了基于 Web 的调试器,将在那里报告错误.新的观察者进程也更智能,如果服务器进程死了也不会退出.相反,它将继续监视源文件,并尝试在进行更多更改后再次启动服务器.

新的 Flask CLI

Flask CLI 比 Flask-Script 更好吗?让我们首先回顾一下所有 Flask 应用程序获得的默认命令,看看它们的性能如何.

当你安装Flask 0.11或更高版本时,您将在虚拟环境中安装一个flask命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(venv) $ flask
Usage: flask [OPTIONS] COMMAND [ARGS]...

This shell command acts as general utility script for Flask applications.

It loads the application configured (through the FLASK_APP environment
variable) and then provides commands either provided by the application or
Flask itself.

The most useful commands are the "run" and "shell" command.

Example usage:

set FLASK_APP=hello.py
set FLASK_DEBUG=1
flask run

Options:
--version Show the flask version
--help Show this message and exit.

Commands:
db Perform database migrations.
run Runs a development server.
shell Runs a shell in the app context.

使用 Flask-script 时,你必须创建一个驱动程序脚本,通常称为 manage.py. 查看上面的内容,可以看到./manage.py runserver映射到flask run, ./manage.py shell 映射到flask shell没什么区别对吧?

这里其实有一个令人困扰的问题. manage.py和flask命令在发现Flask应用程序实例的方式上有所不同. 对于Flask-Script,应用程序作为参数直接或以应用程序工厂函数的形式提供给Manager类. 但是,新的FlaskCLI期望应用程序实例在flask_app环境变量中提供,通常将其设置为定义它的模块的文件名. Flask将在该模块中查找应用程序或应用程序对象,并将其用作应用程序.

不幸的是,在新的 Flask CLI 中没有对应用程序工厂功能的直接支持.使用工厂函数需要遵循的方法是定义一个调用工厂函数来创建app对象的模块,然后在 flask_app中引用该模块.这在概念上类似于 Django 应用程序中的 wsgi.py 模块.

如果你希望为Flasky支持新的 Flask CLI,可以编写一个flasky.py文件,如下所示:

1
2
3
4
import os
from app import create_app

app = create_app(os.getenv('FLASK_CONFIG') or 'default')

注意,我从manage.py复制了这些行,一旦完全迁移到新的CLI,我们就不会使用这些行.

“flask run” 命令

添加flasky.py模块后,可以使用以下命令启动 Flask 开发服务器:

1
2
$ export FLASK_APP=flasky.py
$ flask run

如果你使用的是 Windows,则 FLASK_APP环境变量的设置方式略有不同:

1
2
> set FLASK_APP=flasky.py
> flask run

flask run命令具有启用或禁用重新加载程序和调试器的选项,还可以设置 IP 地址和端口,以便服务器侦听客户端请求. 这些选项与 Flask-script 中的选项并不相同,但它们比较接近.你可以使用flask run --help看见所有可用的选项.

正如我敢肯定,大家都知道,大多数 Flask 的应用程序有这行代码在主脚本的底部:

1
2
if __name__ == '__main__':
app.run()

当你不使用任何 CLI 支持时,这就是你实际如何启动服务器的方法.如果你将此保留在脚本中并开始使用新的 CLI ,不会导致任何问题,但实际上并不需要它,因此你可以删除它.

“flask shell” 命令

shell命令基本上是相同的,但是在如何定义要自动导入到 shell 上下文中的其他符号方面有一点差异. 这是一个在处理应用程序时可以节省大量时间的特性.通常,你可以在 shell 的测试或调试会话中添加模型类,数据库实例和其他可能与之交互的对象.

对于 Flask-Script 来说, Flasky 应用有下述 shell 上下文的定义:

1
2
3
4
def make_shell_context():
return dict(app=app, db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post, Comment=Comment)
manager.add_command("shell", Shell(make_context=make_shell_context))

正如你在上面看到的, make_shell_text()函数在定义 shell 选项的 add_command 调用中被引用. Flask-Script 在启动shell会话之前调用此函数,并在其中包含字典中返回的所有符号.

Flask CLI提供了相同的功能,使用装饰器来标识提供 shell 上下文项的函数(实际上,可以有多个函数)。为了具有与上面相同的功能,我扩展了flasky.py模块:

1
2
3
4
5
6
7
8
9
10
import os
from app import create_app, db
from app.models import User, Follow, Role, Permission, Post, Comment

app = create_app(os.getenv('FLASK_CONFIG') or 'default')

@app.shell_context_processor
def make_shell_context():
return dict(app=app, db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post, Comment=Comment)

因此,如你所见,该函数是相同的,但需要@app.Shell_Context_Processor装饰器,以便 Flask 知道它.

来自 Flask Extensions 的命令

Flask-Script 成功的一个重要原因是它允许其他 Flask 扩展添加自己的命令.实际上,我已经利用了这个功能在我的Flask-Migrate扩展.

那么,如何将这些命令迁移到Click CLI呢?不幸的是,你只能依赖扩展作者为你进行迁移.如果你使用的任何扩展,与 Flask-Script 协同工作,并且还没有更新到与Flask CLI协同工作的话,那么你将非常不幸,你将需要继续使用 Flask-Script.至少在与相关扩展交互时是这样.如果你使用 Flask-Migrate,你不必担心,因为我已经更新了它以支持 Click 和 Flask-Script CLIs,确保您正在使用最新版本.

特别是对于Flask-Migrate,./manage.py db.....在 Flask-script 版本中, Flask-Migrate 扩展是在manage.py中初始化的.对于 Flask CLI 版本,我们可以将其移到新的flasky.py模块,我相信你现在已经意识到它正在成为 Flask-Script 的manage.py的替代品.唯一的区别是它不是直接执行的脚本.下面是添加了Flask-Migrate集成的模块:

1
2
3
4
5
6
7
8
9
10
11
12
import os
from app import create_app, db
from app.models import User, Follow, Role, Permission, Post, Comment
from flask_migrate import Migrate

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)

@app.shell_context_processor
def make_shell_context():
return dict(app=app, db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post, Comment=Comment)

作为例子,你可以使用以下命令将 Flasky 数据库升级到最新的修订版本,但请记住,需要设置FLASK_APP才能使其工作:

1
$ flask db upgrade

应用命令

Flask-Script 的另一个不错的特性是,它允许您将自定义任务编写为函数,然后通过添加装饰器将其自动转换为 CLI 中的命令.在 Flasky 中,我使用这个特性添加了一些命令.例如,下面是我如何实现 Flask-script 的./manage.py test命令:

1
2
3
4
@manager.command
def test(coverage=False):
"""Run the unit tests."""
# ...

@manager.command装饰器是将函数公开为 test命令所需的,它甚至可以检测到coverage参数,并将其添加为 –coverage 选项.

基于装饰器的命令实际上是 Click 的面包和黄油,所以这是可以毫无问题地迁移的.Flask CLI 的等效函数可以放在 flasky.py中,如下所示:

1
2
3
4
5
6
7
8
9
import click

# ...

@app.cli.command()
@click.option('--coverage/--no-coverage', default=False, help='Enable code coverage')
def test(coverage):
"""Run the unit tests."""
# ...

@app.cli.command()装饰器为 Click 提供了一个接口.在Flask-script中,命令是在安装了应用程序上下文的情况下执行的,但是在使用 Flask CLI 的情况下,如果你不需要的话,你可以禁用它. 函数的实际代码不需要更改,但请注意,--coverage选项需要使用@click.OptionDecorator显式地给出,这与Flask-Script不同,后者的选项自动从函数参数列表派生.

另外两个在我的 Flasky 的应用程序中的自定义函数也可以采用相同的方法. flasky.py模块的完整实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os
from app import create_app, db
from app.models import User, Follow, Role, Permission, Post, Comment
from flask_migrate import Migrate
import click

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)

@app.shell_context_processor
def make_shell_context():
return dict(app=app, db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post, Comment=Comment)

@app.cli.command()
@click.option('--coverage/--no-coverage', default=False, help='aaa')
def test(coverage=False):
"Test coverage"
# ...

@app.cli.command()
@click.option('--length', default=25, help='Profile stack length')
@click.option('--profile-dir', default=None, help='Profile directory')
def profile(length, profile_dir):
"""Start the application under the code profiler."""
# ...

@app.cli.command()
def deploy():
"""Run deployment tasks."""
# ...

在本文中,我向您展示了新 Flask CLI 提供的功能,如果你想迁移基于 Flak-Script 的应用程序,这些功能将非常有用.还有几件事你可以做,但我没有在这里写到.文档描述了如何创建自己的驱动程序脚本,类似于 flask-script 的manage.py,以防你不想使用flask命令.它还告诉你如何通过项目的setup.py文件中定义的”entry points”来注册命令,如果你在项目中使用它的话.如果你感兴趣,请务必查看CLI文档以了解这些内容.