tornado写博客碰到的一些问题

最近还是打算继续写个博客练手,不过为了增加一些难度,需要自己实现登录、注册、权限管理、后台管理等功能。tornado不像django那样什么都给了, 也不像flask那样丰富的插件支持,不过也有一些github项目可以作为参考。慢慢记录一下实现过程中碰到的一些小问题。博客采用tornado、mongodb(motorengine) redis(session模块)等实现,参考了http://codingpy.com 的代码,这个网站叫做编程派,用flask写的,我看源码里头用到了一些flask插件,不得不说 flask这种插件扩展真滴很强大,很方便。


如何加密用户密码?

这里使用bcrypt模块, 参考http://stackoverflow.com/questions/9559549/how-to-store-and-compare-password-in-db-using-py-bcrypt
大致思路如下,tornado官方的blog demo里边就是这种方式加密。

def gen_hash(password):
    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())  # encode


def check(password, password_hash):
    return password_hash == bcrypt.hashpw(password.encode('utf-8'),
                                          password_hash.encode('utf-8'))


def test_check():
    ori_pwd = '1234567'
    input_pwd = '1234567'
    assert check(input_pwd, gen_hash(ori_pwd)) is True

如何实现身份识别?cookie?session?

http://stackoverflow.com/questions/15254538/standard-way-to-handle-user-session-in-tornado
网上可以搜到一些tornado基于redis实现的session模块。我觉得可以这么实现,每个用户用uuid4生成一个唯一的uuid串,请求的时候带上它,后台可以用redis存储 一个键值对,uuid作为键,用户数据库的id(mongo里的_id)作为值,每次从uuid拿到用户id,再去数据库取得用户信息。这样就实现了认证。(貌似「礼物说」后台就是 这么搞的),这个uuid是没法造假的,几乎是唯一的,所以没办法伪造一个已经后台已经生成的uuid。 或者直接用set_secure_cookis方法直接把用户的id设置成cookie, 每次取cookie后直接通过id拿到用户信息,这个secure_cookie是加密的,可以保证用户id不会泄露。 这样一来就免去了后端存储session的麻烦,个人倾向于这种方式。


请求参数验证

一般来说网址都是从页面点击的,但是也有可能被随意输入。比如这样一个url:http://xxx.com/page/1,如果用户这么输入http://xxx.com/page/0,我第一次写分页的时候直接报了一个异常,一般分页查询时使用query.skip((page-1)*limit)计算,skip的参数不能是小于0的,所以可能需要验证url里的page值。可以单独抽出来Pagination类处理分页。对于数据的验证,我搜了一下,有三个做的不错的,star数目和贡献者比较多,用起来放心。这样可以在进入数据库model层之前确保不会传给非法的参数。如果不验证参数,就得在model层做一些异常处理,我目前也不太清楚到底哪个实践比较好,可以参考参考防御性编程的一些做法。

http://cerberus.readthedocs.org/en/latest/usage.html

https://github.com/keleshev/schema

https://github.com/alecthomas/voluptuous.git


表单验证

验证一般有前端和后端验证,前端可以通过js插件实现,后端为了安全性也要进行验证。自己也可以实现个验证器,不过逻辑可能比较复杂,python已经有了相应库 WTForms, 包括了Forms、Fields、Validators,Widgets等api,可以方便实现后台表单验证功能。另外csrf因为tornado自带了,所以可以直接用tornado的。或者可以直接用wtforms-tornado。


权限管理

这个是比较头疼的问题。http://damnever.github.io/2015/09/19/how-to-design-a-permission-system/ 可以参考下这篇文章。大致思想就是用二进制数方便实现权限验证,把每个权限当成一个二进制位,这些权限可以组合相加成各种数,将数字和权限做与运算, 得到的结果不为0就说明有相应的权限。具体可以参考以上面的文章。


测试问题

异步的测试相对麻烦一些,不过tornado的testing模块提供了一个方便的测试类AsyncTestCase,这样我们加上装饰器gen_test可以实现异步代码测试。下边这个类针对motorengine测试了一下crud等功能,测试采用py.test框架,支持unittest的运行。用这个主要是为了偷懒,不用一堆assertEqual啥的,都用assert就可以:

class TestModelPost(AsyncTestCase):
    """Post测试
    注意改变数据结构后先删除mongo数据库,否则会有超时
    """

    def setUp(self):
        self.io_loop = IOLoop.current()
        connect(CONFIG.MONGO.DATABASE, host=CONFIG.MONGO.HOST,
                port=CONFIG.MONGO.PORT,
                io_loop=self.io_loop)    # connect mongoengine

    @gen_test
    def test_create(self):
        """非幂等,对应http中的post"""
        user = yield User.objects.create(**save_user)
        save_post['author'] = user
        post = yield Post.objects.create(**save_post)
        assert post is not None
        assert post.slug == 'test'

        post_nums = yield Post.objects.filter(slug='test').delete()
        user_nums = yield User.objects.filter(slug=save_user['slug']).delete()
        assert post_nums == 1
        assert user_nums == 1

    @gen_test
    def test_save(self):
        """幂等,对应http中的put"""
        post = yield Post.objects.create(**save_post)
        assert post.slug == 'test'
        post.slug = 'save'
        post = yield post.save()
        assert post.slug == 'save'
        nums = yield Post.objects.filter(slug='save').delete()
        assert nums == 1

    @gen_test
    def test_bulk_insert(self):
        post_list = []
        n = 10
        for i in range(n):
            post = copy.deepcopy(save_post)    # note use deepcopy
            post['slug'] = post['slug'] + str(i)
            post_obj = Post(**post)
            post_list.append(post_obj)
        yield Post.objects.bulk_insert(post_list)
        post_obj_list = yield Post.objects.find_all()
        assert n == len(post_obj_list)
        nums = yield Post.objects.delete()
        assert nums == n

暂时先写这么多,后面碰到问题了再记录下。

Tornado Web Server
Internals
from Praveen Gollakota