Django实现读取数据库时自动加密解密

一、引言
最近在开发一个网站,使用的是Python2.7+Django,在向BAE发布的时候,数据库出现了异常,原因是BAE基础版提供的免费数据库中,不能插入含有数据库关键字的字段,所以想出了通过使用ROT13算法对字段的内容进行转换后在添加进数据库的办法。

二、正文

  1. 实现Rot13算法

    ROT13算法比较特殊,加密和解密都是同一个算法。如果你要换成其他加密解密算法,请注意区分加密和解密。

    ROT13 = string.maketrans(\
             "ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz",\
             "NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm")
    
    def rot13_encode(text):
        return string.translate(text, ROT13)
    

    点击此处阅读详细内容

  2. object(新式类)

    实现自动加密解密的功能需要对新式类有一个最基本的了解。
    新式类是python2.2版本中引入的,所有新式类全部继承自object类。
    为什么要引入新式类呢?官方的解释是:

    为了统一类(class)和类型(type)

    这个和我们今天的内容没有太大关系,如果有兴趣可以自己问度娘。
    我们只需要知道新式类中添加了__getattribute____setattr__方法。

    每次访问新式类的实例的属性时,都会调用__getattribute__方法。
    而通过__setattr__方法,我们可以实现在程序运行时通过字符串动态的设置实例属性的功能,大大的方便我们的开发。

    getattribute

    #coding=utf-8
    
    class A(object):    #类A继承自object,是一个新式类
        def __init__(self, id=100):
            self.id = id
    
    a = A()
    
    print a.id
    output: 100
    
    print a.__getattribute__('id')  #等价于a.id
    #output: 100
    

    我们尝试着对__getattribute__重写

    #coding=utf-8
    
    class A(object):    #类A继承自object,是一个新式类
        def __init__(self, id=100):
            self.id = id
    
        def __getattribute__(self, attr):
            #注意:我们使用A父类的__getattribute__方法获取属性
            #如果直接使用getattr(self, attr)获取attr属性会造成死循环
            #有兴趣可以尝试一下
            return object.__getattribute__(self, attr) + 10
    
    a = A()
    
    print a.id
    #output: 110
    
    print object.__getattribute__(a, 'id')
    #output: 100
    

    我们可以看出访问a的属性id时,调用了a__getattribute__方法,得到的值是经过处理的,而object.__getattribute__方法可以获取到真正的内容,请记住这两个方法和它们的特性,一会儿我们会用到(这两个方法就是这篇文章最核心的内容)。
    setattr
    虽说__getattribute__是主角,但是少了__setattr__,我们的功能虽然也能完成,但是会很麻烦。

    #coding=utf-8
    
    class A(object):
        def __init__(self, id = 100):
            self.id = id
    
    a = A()
    
    print a.id
    #output: 100
    
    a.__setattr__('id', 120)
    
    print a.id
    #output: 120
    

    好了,如果你学会了如何使用__getattribute__和__setattr__方法,那我们的预备工作就做好了,下面进入正戏。
    字段插入数据库时自动加密
    通过对models.py中模型的save函数进行了重写,我们可以实现在保存时自动调用ROT13算法进行转换的功能。
    Article模型为例:

    #coding=utf-8
    from django.db import models
    
    #实现ROT13算法
    #当然,真正开发时该算法肯定不会写在models.py里
    #这里为了简化代码,直接写在了这里
    ROT13 = string.maketrans(\
             "ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz",\
             "NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm")
    def rot13_encode(text):
        return string.translate(text, ROT13)
    
    
    class Article(models.Model, object):
        #以下为最初的字段(仅挑选出了相关字段)
        title = models.CharField(max_length=200)
        content = models.TextField()
        hot = models.IntegerField(default=0) #文章的热度,此字段无需加密
    
        #使用以下字段记录相关字段是否是加密状态
        #使用"{name}_en"的方式命名,便于操作
        title_en = models.BooleanField(default=False)
        content_en = models.BooleanField(default=False)
    
        #encrypt_items记录哪些字段需要加密
        encrypt_items = ['content', 'title']
    
        #重写Model的save函数,实现当向数据库插入字段时自动加密的功能
        def save(self, *args, **kwargs):
            for attr in self.encrypt_items:
                if not getattr(self, '%s_en' % attr):
                    self.__setattr__(attr, rot13_encode(getattr(self, attr)))
                    self.__setattr__('%s_en' % attr, True)
            #调用父类Model的save函数,进行真正的数据库插入操作
            super(Article, self).save(*args, **kwargs)
    

    从数据库读取时自动进行解密
    重写save函数的不足
    一般我们用读取数据库内容时会这样写:

    art = Article.object.get(id=1)
    title = art.title
    content = art.content
    

    但是在上一小节,重写过save后,读取数据库内容时我们需要这样写:

    art = Article.object.get(id=1)
    title = rot13_encode(art.title)
    content = rot13_encode(art.content)
    

    这样的话写代码的工作量会剧增,而且如果其他代码已经写好,那就需要一行一行的改代码——非常痛苦的工作。所以我们需要对Article进行改进。
    实现读取数据库时自动解密

    #coding=utf-8
    from django.db import models
    
    #实现ROT13算法
    ROT13 = string.maketrans(\
             "ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz",\
             "NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm")
    def rot13_encode(text):
        return string.translate(text, ROT13)
    
    #简化函数名称,便于调用
    OGA = object.__getattribute__
    REP = rot13_encode
    
    class Article(models.Model, object):
        title = models.CharField(max_length=200)
        content = models.TextField()
        hot = models.IntegerField(default=0) #文章的热度,此字段无需加密
    
        title_en = models.BooleanField(default=False)
        content_en = models.BooleanField(default=False)
    
        #encrypt_items记录哪些字段需要加密
        encrypt_items = ['content', 'title']
    
        def __getattribute__(self, attr):
            try:
                if (attr in OGA(self, 'encrypt_items')) and OGA(self, '%s_en' % attr):
                    #若所查询属性(字段)需要加密且已经被加密,解密后再返回
                    return REP(OGA(self, attr))
                else:
                    #所查询属性无需加密或未被加密,直接返回
                    return OGA(self, attr)
            except AttributeError as err:
                #可能出现AttributeError异常,捕获后直接上抛即可
                raise err
    
        #重写Model的save函数,实现当向数据库插入字段时自动加密的功能
        def save(self, *args, **kwargs):
            for attr in self.encrypt_items:
                if not OGA(self, '%s_en' % attr):
                    #注意:这里注释掉了进行加密的这行代码。只有这样才能正常加密!!!
                    #请往下阅读,我会说明原因
                    #self.__setattr__(attr, REP(OGA(self, attr)))
                    self.__setattr__('%s_en' % attr, True)
    
            #调用Model的save函数
            super(Article, self).save(*args, **kwargs)
    

    有的读者可能会疑惑,为何去掉了那行真正进行加密的代码,才能正常加密呢?(没有疑惑的话,这篇文章就已经结束了,点个赞再走呗~)
    最初我自己实现这个功能的时候并没有去掉那行代码(废话!),然而当我测试时发现数据库中需要加密的字段并没有被加密,但是title_encontent_en的值都是1(数据库中1代表True,0代表False)。
    有问题自然要进行调试,我修改了一下代码的最后几行:

    print OGA(self, 'title')
    super(Article, self).save(*args, **kwargs)
    print OGA(self, 'title')
    

    即在调用Modelsave函数前分别打印了一次title真正的值,发现在调用save前,title是被正常加密了的(注意,我这时没有注释掉那一行代码)。但是调用save后就很有趣了,title真正的值又变成了未加密的值。
    而当注释掉那行代码后,数据库中的值才是被加密过后的。对此,我的猜想(没错,是猜想)是在调用save函数时,Django使用了类似这样的代码:

    self.title = self.title
    

    对,你没有看错,就是这样,你要知道,我们对Article__getattribute__方法进行了重写,本来title是加密了的,但是一旦使用一次self.title = self.title,title就会被解密,这样一来,插入到数据库中的字段就是被解密了的。

作者:LucianoSimon
链接:https://www.jianshu.com/p/193fd76e7a96
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

你可能感兴趣的:(随笔,数据库)