整理一下这些天研究web.py的一些经验,写一篇具有划时代意义的指南性说明~哈哈,开个玩笑,谨以此文献给所有学习web.py的同学以及Aaron Swart.
web.py是一个开发web应用的python框架,相比于著名的Django与TurboGears,web.py更加让人感觉是用python在写网站。没有复杂的语法规则,简单的一个实现http协议的框架,不依赖其他packet,不依赖操作系统。当然也是有弊端的,框架只实现了基础的web功能,很多功能需要自己动手写,不像php那样一两个函数就搞定任务。
正如我上句话说的,web.py十分简单,安装只需要sudo easy_install web.py即可,不到2秒中,框架已经躺在服务器里了。如果你没有安装easy_install(比如windows环境),也可以手工安装。github上下载源码,直接安装:
1 |
python ./setup.py install |
装好以后试一下,import web,没有抛错说明安装成功了。
关于web.py的基础使用方式,一个事例文件hello world即可说明一切:
06 |
app = web.application(urls, globals ()) |
12 |
return 'Hello, ' + name + '!' |
14 |
if __name__ = = "__main__" : |
以上代码保存为app.py,然后运行:python ./app.py
默认会监听8080端口作为web服务运行的端口,访问http://127.0.0.1:8080/即可看到Hello world。
看到事例程序,urls是全局的url规则,'/(.*)'是一个正则,匹配用户访问时的url,'hello'就是处理的类名字。也就是说获得url以后与(.*)匹配,匹配上了就调用hello类处理请求。这里的正则是.*,匹配所有字符,所以用户的一切请求都会交给hello类来处理。
根据MVC架构的思路来想,一般把这个hello类叫做控制器,controller。如果请求很多的时候,不要把所有的控制器类都放在一个文件里。我们可以使用python包,比如每个处理类一个文件,读放在action文件夹下,那么我们的urls就这么写:
2 |
'/msg/?' , 'action.msg.msg' , |
3 |
'/login(/quit|/)?' , 'action.login.login' , |
4 |
'/log/?' , 'action.log.log' , |
5 |
'/file/?' , 'action.upload.upload' , |
6 |
'.*' , 'action.show.show' |
action是包名字,msg是文件名,后一个msg是类名。
接下来,处理好了url规则,事例文件调用了web.application创建了一个app对象,第一个参数就是urls,第二个参数是globals()的返回值。
然后就可以app.run()了,一切从此时开始。
不过刚才把请求交给hello类来处理,那么我们看看hello类。所有的控制器类,都可以定义两个函数,GET和POST,顾名思义,这两个函数就用来处理get和post请求。也就是说,用户对app的get请求会交给hello类的GET函数,post请求交给POST函数。
GET(POST)函数的参数是urls中正则部分的匹配到的值。例子里的name就是(.*)的值。你访问http://localhost:8080/phithon,就能看到输出了Hello, phithon.
再深一点,在写网站的时候,哪几个部分最重要?无非是数据库增删改查、访问控制(session)、前端(模板)。那么我一个一个来说。
先说说SESSION吧,session是安全区分访问者的唯一的方法,其他方式用户都能够伪造,只有session是用户不能够修改的。所以,我一般会把一些重要信息记录在session中,比如用户是否登录、用户id、用户权限等。php中session就是一个全局的数组$_SESSION,在web.py中,session是web.ctx中的一个对象(关于web.ctx,请查看cookbook)。
我们在应用运行前首先需要获得一个SESSION对象赋值给web.ctx.session:
01 |
web.config.session_parameters[ 'cookie_name' ] = 'py_pytalk_sid' |
02 |
web.config.session_parameters[ 'cookie_domain' ] = None |
03 |
web.config.session_parameters[ 'timeout' ] = 3600 |
04 |
web.config.session_parameters[ 'ignore_expiry' ] = True |
05 |
web.config.session_parameters[ 'ignore_change_ip' ] = True |
06 |
web.config.session_parameters[ 'secret_key' ] = '3u12m8xXo0is' |
07 |
web.config.session_parameters[ 'expired_message' ] = 'Session expired' |
08 |
session = web.session.Session(app, web.session.DiskStore( 'data/sessions' ), initializer = { 'login' : False }) |
10 |
web.ctx.session = session |
11 |
app.add_processor(web.loadhook(session_hook)) |
众所周知session是用cookie来传递的,所以cookie_name指这个cookie的名字。timeout指session过期时间,秒为单位。secret_key是salt,加密session id。其他一些设置的意义如下:
cookie_name - name of the cookie used to store the session id
cookie_domain - domain for the cookie used to store the session id
timeout - number of second of inactivity that is allowed before the session expires
ignore_expiry - if True, the session timeout is ignored
ignore_change_ip - if False, the session is only valid when it is accessed from the same ip address that created the session
secret_key - salt used in session id hash generation
expired_message - message displayed when the session expires
我就不再赘述了。通过web.session.Session初始化一个session对象,DiskStore是储存session文件的地址,initializer是session对象初始化内容。最后将新建的这个session对象赋值给web.ctx.session,以后就能够直接调用web.ctx.session来访问session了。
注意,这些设置请在app.run()函数调用前设置好,然后调用app.run()执行程序。以后我们的web.ctx.session的使用就和php中的$_SESSION数组一样了。比如用户登录以后,设置web.ctx.session.uname = '用户名'。访客访问时,判断web.ctx.session.login == True。
关于访问控制,还有一个小技巧。通常一个网站有后台,后台的页面也不止一个,这时候访问控制就是一个大麻烦。
如果后台页面很多,我们不可能在每个页面对应的类中判断web.ctx.session.login是否为真,来判断管理员是否登录。所以我们可以让所有后台页面对应的类都继承一个admin类,然后在admin类的构造函数里加入判断代码:
3 |
if not web.ctx.session.login: |
4 |
raise web.seeother( '/login' ) |
如果web.ctx.session.login的值非真,就用raise语句抛出一个错误,并跳转到/login页面去登录。如果不停止运行的话,即使调用seeother,但后面的内容还是会被执行,造成了安全隐患。但这里不能用return,return没任何效果,也不能用sys.exit,否则就直接退出整个网站的运行了。raise是一个好方法,可以完美保证后面的代码不被执行。
然后说到数据库。数据库是一个比较容易出漏洞的操作(sql注入),但解决sql注入的方法又是极为简单的(可以说是所有漏洞里最好解决的),那就是参数化查询。web.py提供了一个类似参数化查询的方式,基本可以满足我们日常使用数据库。
首先我们创建一个数据库对象,并连接数据库:
1 |
database = 'db/pytalk.db3' |
2 |
db = web.database(dbn = 'sqlite' , db = database) |
我使用的sqlite数据库,如果是mysql,方法类似具体看文档。
这个db就是sql对象,我们以后就调用db.query来执行sql语句:
1 |
res = db.query( "SELECT * FROM `log` WHERE `sort` = $i AND `keyword` = $search" , vars = { |
用$xxx来占位,然后用一个字典对象来传入数据。这样web.py内部会自动将相应的占位符用具体的数据替代。不知道大家注意没有,$search外面是没打引号的,也就是说web.py会自动帮我加入引号,这也是为什么它能够防范sql注入,因为它能自动处理引号和转义字符。
这样执行sql语句以后,我就再也不用担心注入的问题了,从用户那里获取的数据我直接像这样插入数据库,不用过滤,不用像PHP一样调用addslashes处理了。
query返回值是一个iterbetter对象,这个对象是一个迭代器,但不像列表,它内部维护着一个指向当前元素的指针,这个指针只会往后走。也就是说我调用了一次res[0],下次就必须调用res[1],再访问res[0]就会抛出错误。
所以我们可以简单地将这个值转换成一个列表,之后就方便多了:
1 |
res = list (db.query( "SELECT * FROM `log` WHERE `sort` = $i AND `keyword` = $search" , vars = { |
这个列表就是sql语句执行结果,len(res)就是行数,列表中的元素是字典,字典的键是列名,字典的值是列值。
最后说一下模板引擎,也就是关于前端的各种问题。
$def定义一个变量。一般这个变量是一个字典,这样我们把所有模板中可能用到的值都放进字典中,作为一个变量传入。
我在这里定义了两个函数,很大程度上简略了模板操作:
1 |
def assign( self ,key,value = ''): |
3 |
self .tplData = dict ( self .tplData, * * key) |
5 |
self .tplData[key] = value |
7 |
def display( self , tplName): |
8 |
self .tplData[ 'render' ] = web.template.render( 'templates' , globals = self .globalsTplFuncs) |
9 |
return getattr ( self .tplData[ 'render' ], tplName)( self .tplData) |
self.tplData是包含所有变量的字典。我们使用assign来定义模板变量,定义完所有模板变量以后,调用display显示模板。tplName就是模板的名字(模板文件后缀是html),templates是模板文件地址。
globalsTplFuncs是模板函数,比较简单的模板是用不上的。
关于安全性,我强调一点。在模板引擎中,web.py是默认会转义<>"'&等xss字符的,也就是说输出$data['name']的时候会转换这个值再输出。如果你不想自动转义,就在$后面加个冒号,$:data['name']就不会转义了。
什么情况下不想转义,就是模板存在包含的时候(如果转义的话你包含的文件就不是html了)。比如我写一个网站,网站的header一般是不会变的,所以我们最好在模板中创建一个header.html,然后其他模板文件包含之。这就就不用每写一个页面都写html头了。
那么怎么实现包含?看到我之前那个display函数的第一行,就是将web.template.render方法赋值给了tplData['render']变量。然后我们在模板中就能够直接调用$:tplData['render'].header(tplData)来包含header.html,并将tplData传入。
具体代码可以参考我后面给的一个app。
说了这么多,可能有的同学还有不少疑惑。其实千万文字不如几行代码,我把我自己做的一个项目开源出来,相信有什么疑惑的方面,在代码中也能迅速找到解答。如果涉及到代码以外的东西,我们可以私下交流。
已在在线IRC聊天室的小程序。