整理一下这些天研究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上下载源码,直接安装:
python ./setup.py install
装好以后试一下,import web,没有抛错说明安装成功了。
关于web.py的基础使用方式,一个事例文件hello world即可说明一切:
import web
urls = (
'/(.*)', 'hello'
)
app = web.application(urls, globals())
class hello:
def GET(self, name):
if not name:
name = 'World'
return 'Hello, ' + name + '!'
if __name__ == "__main__":
app.run()
以上代码保存为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就这么写:
urls = (
'/msg/?', 'action.msg.msg',
'/login(/quit|/)?', 'action.login.login',
'/log/?', 'action.log.log',
'/file/?', 'action.upload.upload',
'.*', '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:
web.config.session_parameters['cookie_name'] = 'py_pytalk_sid'
web.config.session_parameters['cookie_domain'] = None
web.config.session_parameters['timeout'] = 3600
web.config.session_parameters['ignore_expiry'] = True
web.config.session_parameters['ignore_change_ip'] = True
web.config.session_parameters['secret_key'] = '3u12m8xXo0is'
web.config.session_parameters['expired_message'] = 'Session expired'
session = web.session.Session(app, web.session.DiskStore('data/sessions'), initializer={'login': False})
def session_hook():
web.ctx.session = session
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类的构造函数里加入判断代码:
class admin:
def __init__(self):
if not web.ctx.session.login:
raise web.seeother('/login')
如果web.ctx.session.login的值非真,就用raise语句抛出一个错误,并跳转到/login页面去登录。如果不停止运行的话,即使调用seeother,但后面的内容还是会被执行,造成了安全隐患。但这里不能用return,return没任何效果,也不能用sys.exit,否则就直接退出整个网站的运行了。raise是一个好方法,可以完美保证后面的代码不被执行。
然后说到数据库。数据库是一个比较容易出漏洞的操作(sql注入),但解决sql注入的方法又是极为简单的(可以说是所有漏洞里最好解决的),那就是参数化查询。web.py提供了一个类似参数化查询的方式,基本可以满足我们日常使用数据库。
首先我们创建一个数据库对象,并连接数据库:
database = 'db/pytalk.db3'
db = web.database(dbn = 'sqlite', db = database)
我使用的sqlite数据库,如果是mysql,方法类似具体看文档。
这个db就是sql对象,我们以后就调用db.query来执行sql语句:
res = db.query("SELECT * FROM `log` WHERE `sort` = $i AND `keyword` = $search", vars = {
'i': 100,
'search': 'test'
})
用$xxx来占位,然后用一个字典对象来传入数据。这样web.py内部会自动将相应的占位符用具体的数据替代。不知道大家注意没有,$search外面是没打引号的,也就是说web.py会自动帮我加入引号,这也是为什么它能够防范sql注入,因为它能自动处理引号和转义字符。
这样执行sql语句以后,我就再也不用担心注入的问题了,从用户那里获取的数据我直接像这样插入数据库,不用过滤,不用像PHP一样调用addslashes处理了。
query返回值是一个iterbetter对象,这个对象是一个迭代器,但不像列表,它内部维护着一个指向当前元素的指针,这个指针只会往后走。也就是说我调用了一次res[0],下次就必须调用res[1],再访问res[0]就会抛出错误。
所以我们可以简单地将这个值转换成一个列表,之后就方便多了:
res = list(db.query("SELECT * FROM `log` WHERE `sort` = $i AND `keyword` = $search", vars = {
'i': 100,
'search': 'test'
}))
这个列表就是sql语句执行结果,len(res)就是行数,列表中的元素是字典,字典的键是列名,字典的值是列值。
最后说一下模板引擎,也就是关于前端的各种问题。
$def with (data)
Hello $data['name']!
$def定义一个变量。一般这个变量是一个字典,这样我们把所有模板中可能用到的值都放进字典中,作为一个变量传入。
我在这里定义了两个函数,很大程度上简略了模板操作:
def assign(self,key,value = ''):
if type(key) == dict:
self.tplData = dict(self.tplData,**key)
else:
self.tplData[key] = value
def display(self, tplName):
self.tplData['render'] = web.template.render('templates', globals = self.globalsTplFuncs)
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聊天室的小程序。
项目地址:http://phith0n.github.io/Pytalk_Irc
演示地址:http://p1ng.pw:81/