版权声明:本文为CSDN博主「一笑照夜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/erwugumo/article/details/95965235
1、web的状态与访问权限
我们在上一章已经完成了日志的记录与SQL的处理。并且可以通过viewlog网址查询所有的日志。但是现在问题来了:作为日志数据这样较为敏感较为有价值的信息,应该是所有人可以随意看到的吗?
不应该。我们应添加一个功能,使得只有认证用户可以查询日志。
最初的想法可以是这样的:在我们的webapp中维护一个全局变量,如果这个变量值为True,说明有权限;如果变量值为False,说明没有权限。
看上去好想可以用。
但是实际上不可以。为什么呢?
之前我们讨论过所谓的变量作用域。即使是代码中的全局变量,它的作用范围也仅限于该程序。你总不能让程序A中的全局变量a在程序B中也可以使用吧。那样程序B不就把程序A给绿了。这不和谐。
那这样,只要有用户在访问我的网站,我的程序A就一直运行,全局变量a就一直在作用域内,一直都是有效的。这样不行吗?
不行。为什么不行?因为你没法保证“程序A一直运行”。
在我们自己的电脑上,我们打开一个程序,不自己关闭它,它就一直在内存里,但是在服务器上不是。服务器会根据需要,随时运行你的代码,当然也可能会随时关闭。这样变量作用域就会时有时无,一会是True一会是False。这样用户可能一会可以看,一会又不能看。很麻烦。
那我们把这个变量保存在SQL中吧。需要验证权限的时候就读取一下。
这样是可行的,而且在你的网站仅有一个用户的时候完全可行。这个唯一的用户对应唯一的变量,根据用户是否登录与登出改变变量的值。听起来好惨
要是用户多了就很麻烦。每个用户都要维护这样一个变量,有一种大炮打蚊子的感觉。
为什么会出现这种情况呢?因为在服务器看来,使用web时保存变量是一个很浪费的操作,为了实现高并发,就很难有保存变量的效率空间了。二者不可得兼。web选择了效率,因此它自己不会保存变量。这里谈到的变量,专业词汇应该为“状态”。web是无状态的。
那我们要用什么方法实现权限检查啊?也就是说,我们要用什么方法分别保存多个用户的变量啊?
大多数web应用开发框架都提供了一种名为session(会话)的技术来满足这个需求。
可以把会话看作是无状态web上面的一种状态。
session的原理:服务器给用户一小段认证数据,cookie,并且在服务器上建立一个与cookie对应的一段认证数据,会话ID。这样就可以让每个用户都有和服务器的唯一连接,可以持久储存数据,而且不同的用户也不会混淆了。
2、session的机制
我们提供了一段成品代码来演示session的会话机制如何工作。
from flask import Flask,session
app=Flask(__name__)
app.secret_key='YouWillNeverGuess'#秘密密钥
@app.route('/setuser/<user>')
def setuser(user:str)->str:
session['user']=user
return 'User value set to: '+session['user']
@app.route('/getuser')
def getuser()->str:
return 'User value is currently set to:'+session['user']
if __name__=='__main__':
app.run(debug=True)
首先要从flask中导入session模块。不用对session很害怕,可以把session看作是一个全局的python字典,我们用到的功能就是保存变量罢了。flask可以保证,无论应用的代码加载和卸载多少次,session中的变量都能够一直存在。
其次,存储在session中的所有数据都有一个唯一的浏览器cookie作为密钥,这就确保了并不是web应用的每一个用户都能访问你的会话数据。为了得到这些密钥,我们需要为flask提供一个秘密密钥作为种子,flask会用这个秘密密钥加密所有的cookie,保护它不被外人刺探。也就是说,我作为一个用户,我的会话数据保存在session中,因为我有cookie,所以我可以看我的数据。我的cookie又是经过flask加密的,这样就减少了泄露的概率。
在设置完秘密密钥之后,就可以直接把session当做一个字典使用了。
@app.route('/setuser/<user>')
这行代码看起来很奇怪,因为之前我们的url中都没有尖括号包括的代码。它的用途是希望用户提供一个值来赋给user变量。得到user变量后,会在setuser函数中使用该变量,并在字典中保存这个值。之后但会保存成功的提示。
注意一点,虽然所有用户共用这一行保存变量的代码,而且表面上看session字典中的键都是user,但并不表示字典中只有一个名为user的键,这个键只有一个值。实际上,不同的用户有不同的键值,但是在这里不需要考虑键名问题,只需要当它是仅为一个用户服务的就好,其他的由session自己完成。
如果我们想查看user这个变量现在的值,访问/getuser这个url即可。它会调用一个函数访问字典中的user键,并返回它的值。
既然我们已经可以实现变量的保存和读取了,那么接下来可以进行测试了。
首先注意一点,web服务器把一个浏览器当做一个用户,因此你电脑上的firefox和chrome会被认作是两个用户。这样就很容易测试了:我们打开多个浏览器分别访问不同的/setuser/<user>,就会保存多个user值,然后在分别查看,如果它们的值不同,就说明已经成功的进行了分别保存。
效果如下:
接下来分别查看这三个user的值,如下:
可以看出,不同的浏览器访问相同的网址,得到了不同的结果,说明变量保存成功,并且未发生混淆。
3、用session来控制登录
使用一个简单的实验代码来进行我们的探索:
from flask import Flask,session
app=Flask(__name__)
app.secret_key='YouWillNeverGuess'
@app.route('/')
def hello()->str:
return 'Hello from the simple webapp.'
@app.route('/page1')
def page1()->str:
return 'This is page 1.'
@app.route('/page2')
def page2()->str:
return 'This is page 2.'
@app.route('/page3')
def page3()->str:
return 'This is page 3.'
if __name__=='__main__':
app.run(debug=True)
可以看到,代码创建了四个url。我们希望page1、page2、page3都只对登录用户可见。因此,我们首先要写一个登录页面。如下:
@app.route('/login')
def do_login()->str:
session['logged_in']=True
return 'You are now logged in'
很简单,实现的功能就是如果你访问了该url,则置session字典中的logged_in为True,并返回一个已登录的提示。
接下来写一个注销页面,如下:
@app.route('/logout')
def do_logout()->str:
#session['logged_in'].clear()
session.pop('logged_in')
return 'You are now logged out.'
注销逻辑有两种:第一种是将logged_in的值由True改成False,第二种是直接删去logged_in。在这里我们选择后者。原因稍后再讲。另外注意一点,python中,删除字典的某一项使用的方法为pop,删除字典内所有数据才会用clear。
然后是一个查看当前状态的页面:
@app.route('/status')
def check_status()->str:
#if session['logged_in']==True:
if 'logged_in' in session:
return 'You are currently logged in.'
return 'You are NOT logged in.'
也有两种逻辑:第一种是判断键值是否为True,第二种是判断键值是否存在。同样选择第二种。为什么都选第二种呢?
因为python的字典机制:如果字典中某个键名不存在,就不能检查它的值。注销和查看状态中的第一种方法都会检查字典中某一键名的值,但是如果键名不存在呢?那样程序就会崩溃。而且我们无法要求用户一定是先访问login再访问status或logout,一旦不按这种顺序访问,网站就会出错,也不会返回You are NOT logged in的信息,那样就很不喜人。
因此我们选择直接判断是否存在,这样就避免了检查键值的操作。
接下来开始测试:
首先我们不登录:
返回正确。
接下来我们登录:
看状态:
注销:
看状态:
这样就简单实现了登陆的操作。
4、引入函数修饰符
在3中,我们实现了登陆与注销,对于status,登陆与注销后,它显示的内容不同,这就是限制访问url的雏形。现在想要实现对所有三个page实现这一点。
当然很容易,把status中的check_status代码在这三个url下面复制一下不就好了。
这样是好了,但也没好。为什么呢?
这样做难以维护,想一下要是我们要想改一下键名,或者更改返回的信息,那该多麻烦!
那把这写代码单独拎出来怎么样?写一个函数,这三个url下面只用一行代码,调用这个函数如何?
本质上没有区别,后者就是把复制黏贴几行代码变成了复制黏贴一行代码,仍然难以维护,而且这两者都会让代码真正要做的工作变得模糊:page1本来是返回一行通知的,你加的这个函数干嘛用啊?劣化了代码的可读性。
如果有一种方法,可以以某种方式为函数添加一个功能,比如说为page1、page2、page3这三个函数增加一个相同的检查状态功能就好了。为函数增加额外功能,这就是函数修饰符做的事情。
利用修饰符,可以用额外的代码增强现有的函数,从而改变现有函数的行为而不必修改它的代码。
也就是说,我现在有一个高达,给他加个喷气模块就能飞,而不用把它大卸八块从内而外的改造才能飞,这个喷气模块就是函数修饰符。
我们之前就有用过函数修饰符:所有的函数修饰符前面都有一个@作为前缀,它们很容易发现。
接下来我们将创建一个自己的函数修饰符。
5、创建函数修饰符的铺垫
创建函数修饰符,需要我们了解三个问题:
如何把一个函数作为参数传递到另一个函数?
如何从函数返回一个函数?
如何处理任意数量和类型的函数参数?
首先来解决第一个问题:如何把一个函数作为参数传递到另一个函数?
咋一眼好像很奇怪?函数的参数也可以是函数吗?当然可以。python中的一切都是对象,函数自然也是对象,可以通过下面的试验代码证明这一点:
hello是一个函数,id是另外一个函数,我们调用id(hello)也会得到一个结果,而不会报错。这里hello就是id的参数。但是注意一点,虽然hello是id的参数,但是id(hello)并没有调用hello,而只是返回了一个地址,实际上,函数可以选择是否调用它的函数参数。我们下面写一个调用函数参数的函数apply。如下:
def apply(func:object,value:object)->object:
return func(value)
它的第一个参数func就是一个函数对象,这里的注解object可以帮助理解这一点,第二个参数value也是一个对象。虽然它们类型相同,但是通过名字可以看出,第一个参数应该是一个函数,而第二个参数应是第一个函数参数所需要的参数。这是约定俗成的起名方法。效果如下:
可以看到apply的确调用了它的函数参数,最后一个我是故意这么写的,有点好奇它会不会死循环。
然而知道参数可以是函数有什么用呢?这个问题暂时按下不表。接下来看第二个问题:如何从一个函数返回一个函数?
如果之前只学过C的话,这是不可想象的。C中return只能是一个值,连数据结构都不可以,怎么还可以是一个函数呢?的确可以。
为了从函数返回一个函数,首先来学习一下嵌套函数的知识。
嵌套函数,顾名思义,就是在一个函数中再定义一个函数,举例如下:
def outer():
def inner():
print("This is inner.")
print("This is outer.")
inner()
在outer内定义一个函数inner,然后就可以在outer内调用这个inner函数了。注意inner的作用域仅在outer内部,你在外面是无法调用inner的。
这有什么用呢?我把inner的代码写在调用的地方不就好了。
的确是这样。但是有一个问题,我们上文说过,函数可以作为返回值,你如何返回一大堆代码呢?这不可能吧。
如果你想返回一个函数中的一部分代码,一个做法就是把这个函数中要返回的这部分代码打包成一个函数,然后返回这个函数。
仍然以上面的函数为例,想返回inner。如下:
def outer():
def inner():
print("This is inner.")
print("This is outer.")
return inner
那么运行它会发生什么呢?如下:
直接运行outer。返回inner的类型和地址。因为outer有返回值,但我们没有指定返回值赋给谁,所以会出现这种情况。
将outer的返回值赋给i。然后输入i,情况和上面的类似。
输入i(),这时才会调用inner。说明只有加上括号了,才能够调用函数对象。只使用函数名就只是返回函数对象的类型和地址。
要想调用outer可以直接调用inner,只需要在return后面加上括号就行了。
这样就体现出嵌套函数的作用了。
第三个问题:如何处理任意数量和类型的函数参数?
假设我现在有一个函数,它可以接受任意多个参数,例如没有参数,一个参数,9527个参数。如何实现呢?总不可能是对每种情况分别写一个函数吧。
python解决这一问题的方法是传入一个参数元组,元组内保存参数。元组内的元素数量可以是任意个,因此函数可以接收任意个参数。这里使用*代表任意数量,如下:
def myfunc(*args):
for a in args:
print(a,end=' ')
if args:
print()
其运行效果如下:
注意,我们是直接输入参数的,而不需要自己先声明一个元组,初始化,然后再将元组作为参数输入。将多个参数变为元组这一步是由解释器完成的,我们在调用时只需要把它看作是能够接收任意个参数即可。而且类型不限哦。
那么问题来了,我提供一个列表行不行。比如说我在上文得到了一个列表,我想用这个函数处理列表中所有元素,难不成还得一个个拆开?当然不用,要想处理列表中的所有参数,只需要在调用时,列表参数前面加一个*即可,函数会自动展开这个列表。
如下:
可以很明显的看出来,前者是直接打印列表,也就是把列表当做一个元素打印;后者则是把列表展开后一个个打印元素。
现在我们更贪心了,可不可以直接指定函数内的若干个变量,直接让参数赋给变量呢?毕竟,如果一个函数的参数太多的话,记住参数顺序也有点烦,要是可以直接指定,那不就不用记住顺序了。
可以的,我们在写vsearch4web函数里就用过这种调用方式,这称为接收一个函数字典。要想让函数接收一个参数字典,需要在参数前面加两个*。如下:
def myfunc(*args):
for a in args:
print(a,end=' ')
if args:
print()
def myfunc2(**kwargs):
for k,v in kwargs.items():
print(k,v,sep='->',end=' ')
if kwargs:
print()
这里的myfunc2就可以接收参数字典。效果如下:
但是注意,这种接收参数的方式只能用于字典,而不能给函数里面的某一个特定参数指定赋值,如下:
def myfunc2(**kwargs):
print(sec)
for k,v in kwargs.items():
print(k,v,sep='->',end=' ')
if kwargs:
print()
报错如下图:
注意这里报的错指的是print(sec)这一行的sec未定义,说明参数中的sec没有被正确赋值,而是被加入到了参数字典中。若想指定sec,需要在定义函数时把参数更改如下:
def myfunc2(sec,**kwargs):
print(sec)
for k,v in kwargs.items():
print(k,v,sep='->',end=' ')
if kwargs:
print()
运行如下:
注意sec在定义函数中必须在kwargs之前,否则会报错。在运行的时候就没有关系了。另外,在调用这个函数的时候也可以用直接传入一个字典,之前传送SQL配置的时候就用过,在这里不再赘述。
现在总结一下,若干参数的传入其实就是在函数中先定义一个列表或者字典,然后用args或者*kwargs来填这个字典。
最后,我们可以把这两者结合,传入一个列表,再传入一个字典,如下:
def myfunc3(*args,**kwargs):
if args:
for a in args:
print(a,end=' ')
if kwargs:
for k,v in kwargs.items():
print(k,v,sep='->',end=' ')
效果如下:
没指定的默认放在args中,指定的就放在kwargs中。
6、创建函数修饰符
前面铺垫了这么一大堆,有什么用呢?在这里就可以描述一下函数修饰符的功能了:
函数修饰符的参数是一个函数,它会自己定义一个函数,称作wrapper(包装),在自己定义的这个函数里调用参数函数,然后返回定义函数。也就是说,自己定义的这个函数wrapper,就是把参数函数包装了一下,然后返回。因此需要用到以上的三大功能:
以函数为参数,从而使得我们可以传入一个函数;
可以返回一个函数,从而使得我们能够得到新的函数;
可以传入任意参数,使得wrapper能够包装任何函数。
下面开始创建函数修饰符。它本质上还是一个函数,它必须维护被修饰函数的签名。什么叫被修饰函数的签名?它返回的函数要和被修饰函数有同样的参数,个数和类型都得相同,参数的个数和类型就叫签名。
代码如下:
from flask import session
from functools import wraps
def check_logged_in(func):
@wraps(func)
def wrapper(*args,**kwargs):
if 'logged_in' in session:
return func(*args,**kwargs)
return "You are NOT logged in"
return wrapper
首先是import,要从flask中引入session,这是要实现函数功能必须的;引入wraps是函数修饰符的要求,这个有点复杂,我们只需要知道要先用一个这个修饰符,然后定义wrap函数。
然后就可以定义我们自己的函数修饰符了,因为是函数,所以也用def定义,参数只有一个,就是被修饰的函数。
接下来调用修饰符,并定义wrap函数,为了使其具有通用性,要用args和*kwargs来让其可以接收任意参数。接下来是重头戏,要给参数函数增加的功能就写在这里面。我们的功能就是判断字典中有没有登录记录,有的话,就返回参数函数,注意这里是有括号的,因此是调用;没有的话,就返回通知。
最后返回定义的函数即可。这里没有括号,因此不是调用。因为我们修饰一个函数,要得到的应该是一个新的函数对象,而不是直接就让被修饰函数调用。
用修饰符修饰函数使得对page1、2、3进行限制访问的代码如下:
from flask import Flask,session
from checker import check_logged_in
app=Flask(__name__)
app.secret_key='YouWillNeverGuess'
@app.route('/login')
def do_login()->str:
session['logged_in']=True
return 'You are now logged in'
@app.route('/logout')
def do_logout()->str:
#session['logged_in'].clear()
session.pop('logged_in')
return 'You are now logged out.'
@app.route('/status')
def check_status()->str:
#if session['logged_in']==True:
if 'logged_in' in session:
return 'You are currently logged in.'
return 'You are NOT logged in.'
@app.route('/')
def hello()->str:
return 'Hello from the simple webapp.'
@app.route('/page1')
@check_logged_in
def page1()->str:
return 'This is page 1.'
@app.route('/page2')
@check_logged_in
def page2()->str:
return 'This is page 2.'
@app.route('/page3')
@check_logged_in
def page3()->str:
return 'This is page 3.'
if __name__=='__main__':
app.run(debug=True)
先引入修饰符,再添加三行@即可,很方便。
7、更新我们的webapp代码
from flask import Flask, render_template,request,redirect,escape,session
from vsearch import search4letters
from DBcm import UseDatabase
from checker import check_logged_in
app=Flask(__name__)
app.secret_key='YouWillNeverGuess'
app.config['dbconfig']={'host':'127.0.0.1',
'user':'vsearch',
'password':'vsearchpasswd',
'database':'vsearchlogDB',}
def log_request(req:'flask_request',res:str)->None:
#with open('vsearch.log','a') as log:
#print(req.form,req.remote_addr,req.user_agent,res,file=log,sep='|')
with UseDatabase(app.config['dbconfig']) as cursor:
_INSERT="""insert into log
(phrase,letters,ip,browser_string,results)
values
(%s,%s,%s,%s,%s)"""
cursor.execute(_INSERT,(req.form['phrase'],
req.form['letters'],
req.remote_addr,
req.user_agent.browser,
res,))
@app.route('/search4',methods=['POST'])
def do_search() -> 'html':
phrase=request.form['phrase']
letters=request.form['letters']
results=str(search4letters(phrase,letters))
log_request(request,results)
return render_template('results.html',
the_title='Here are your results',
the_phrase=phrase,
the_letters=letters,
the_results=results)
@app.route('/')
@app.route('/entry')
def entry_page() -> 'html':
return render_template('entry.html',
the_title='Welcome to search4letters on the web!')
@app.route('/viewlog')
@check_logged_in
def view_the_log()->str:
#contents=[]
with UseDatabase(app.config['dbconfig']) as cursor:
_SELECT="""select phrase,letters,ip,browser_string,results from log"""
cursor.execute(_SELECT)
contents=cursor.fetchall()
titles=('Phrase','Letters','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
@app.route('/login')
def do_login()->str:
session['logged_in']=True
return 'You are now logged in'
@app.route('/logout')
def do_logout()->str:
#session['logged_in'].clear()
session.pop('logged_in')
return 'You are now logged out.'
@app.route('/status')
def check_status()->str:
#if session['logged_in']==True:
if 'logged_in' in session:
return 'You are currently logged in.'
return 'You are NOT logged in.'
if __name__=='__main__':
from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app=ProxyFix(app.wsgi_app)
app.run()