乐正

Actions speak louder than words.

Python的钉钉加密/解密工具

又是很久没有写技术博客了,盖因最近都在学习知识,也没有总结出什么值得分享的内容,所以一直停笔至今。最近的工作和钉钉的开发打上了交到,官方并没有提供任何Python的SDK,于是只能全部自己写。现在我将其中实现起来相对费时间的“加密/解密/签名”部分分享出来,希望能帮助到一些人。

加密/解密的具体机制,可以参考官方文档

在你的项目中安装这个扩展,可以使用: pip install dingtalk_crypto 安装。

使用方法,可以参考下面的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# -*- coding: utf-8 -*-

import json
from dingtalk_crypto import DingTalkCrypto

# 这个是钉钉官方给的测试数据
# @see https://open-doc.dingtalk.com/doc2/detail.htm?treeId=175&articleId=104945&docType=1#s14
encrypt_text = '1a3NBxmCFwkCJvfoQ7WhJHB+iX3qHPsc9JbaDznE1i03peOk1LaOQoRz3+nlyGNhwmwJ3vDMG' \
               '+OzrHMeiZI7gTRWVdUBmfxjZ8Ej23JVYa9VrYeJ5as7XM/ZpulX8NEQis44w53h1qAgnC3PRzM7Zc' \
               '/D6Ibr0rgUathB6zRHP8PYrfgnNOS9PhSBdHlegK+AGGanfwjXuQ9+0pZcy0w9lQ=='

crypto = DingTalkCrypto(
    '4g5j64qlyl3zvetqxz5jiocdr586fn2zvjpa8zls3ij',
    '123456',
    'suite4xxxxxxxxxxxxxxx'
)

signature = '5a65ceeef9aab2d149439f82dc191dd6c5cbe2c0'
timestamp = '1445827045067'
nonce = 'nEXhMP4r'


class TestCrypto:
    def test_decrypt(self):
        randstr, length, msg, suite_key = crypto.decrypt(encrypt_text)
        msg = json.loads(msg)

        assert msg['EventType'] == 'check_create_suite_url'
        assert msg['Random'] == 'LPIdSnlF'
        assert suite_key == 'suite4xxxxxxxxxxxxxxx'

    def test_encode(self):
        encrypt_msg = crypto.encrypt('hello world')
        randstr, length, msg, suite_key = crypto.decrypt(encrypt_msg)
        assert msg == 'hello world'

    def test_check_signature(self):
        assert crypto.check_signature(encrypt_text, timestamp, nonce, signature)

    def test_sign(self):
        msg = crypto.encrypt('hello world')
        actual_sig, actual_time, actual_nonce = crypto.sign(msg)
        assert True

最后,贴出项目的源码地址,希望能一些交流。

平凡与永恒

一直以来都不曾写过与爱有关的主题,自己终归不是伤春悲秋之人,爱情的词汇见得多了,总是觉得矫情。然后,今天这篇定要和“爱”脱不了关系,因为这是和妻子争吵而被要求用以深刻认识自己错误的检讨书。

先来谈谈今次的争吵吧。既然是争吵,便必定有起因,再是夫妻间的争吵,大抵多是源于琐事。

两天前的夜里,我穿梭在狭窄的卧室中寻找着什么物品,具体是什么,现在竟也想不起来了。只记得找了许久都未找到,心中有点恼火。妻子在洗泡在桶里的白毛衣,现在是接近凌晨,季节也已入冬,虽说市政供上了暖气,可是“呼呼”地从窗缝中刮进的北风的屋子难锁住多少暖意。

“老公,帮我烧一壶热水倒到桶里。”

“这么晚,你洗什么衣服啊!”嘴上这么说,却是掩盖心里的不情愿,拒绝的话都不敢直接表达,真是懦弱的性格。

“你不烧我自己去。”说着便拿起热水壶去厨房接水。

这话让我一下子恼羞成怒了,好像觉得自己拙劣的推辞被拆穿:“不能放在洗衣机里洗吗,非要手洗,就你特殊!”

这话点燃了战火,争吵愈烈,持续到第二天早上。直到我答应写检讨书,气氛才缓和一点。深度的认识一番在这么争吵中自己犯下的诸多错误。

第一,嫁怒于人。这条虽不是最严重的,但无疑是一系列争吵的罪魁祸首。第二,无中生有。总是为辩论的胜利,找些莫须有的指责安插在妻子的头上。这条每每也是让刚起端倪的争吵升级的元凶。第三,屡犯不改。这条实是能多次让多次让争吵因类似的原因爆发。其他的原因也有,这三条是最典型的。

俗话说:“要热是火口子,要亲是两口子。”可见两口子和火口子的相似关系。“结发夫妻吵架不记仇”,无论什么样的争论都要能做到“床头打架床尾和”。

说一些我对我们之间感情的期望吧。夫妻生活是平凡的,白头到老的夫妻是伟大的。无数日平凡的生活,组合成伟大而永恒的爱情。我希望我们的爱情是永恒的。这可不是一个简单的目标。不是说两个相爱的人结合便能成就白头到老,在这漫长的时光中,爱情便像一面由两个人共同维护的镜子,有太多的原因可以让它受到伤害,这些伤害积累,像蜘蛛网一样慢慢扩散开。我们俩人,需得小心的呵护,也要不时的擦拭,不能让它蒙尘。

你问我有多爱你,我的回答是非常非常。我问你,得到的也是一样的答案。如此笃定的爱情,依然不能让我在通往永恒爱情的这条路上安心,小心翼翼,如履薄冰。我真是太想和你一起经营这爱情了。我想,你也一定是和我一样的心情吧。依然记得初见你时的心动,而这样的心动,在每次凝望你脸庞的时候都会再次感受到。

亲爱的,我们一起白头到老好吗?

Python 中的关键字with详解

在 Python 2.5 中,with关键字被加入。它将常用的 try ... except ... finally ...模式很方便的被复用。看一个最经典的例子:

1
2
with open('file.txt') as f:
    content = f.read()

在这段代码中,无论with中的代码块在执行的过程中发生任何情况,文件最终都会被关闭。如果代码块在执行的过程中发生了一个异常,那么在这个异常被抛出前,程序会先将被打开的文件关闭。

再看另外一个例子。

在发起一个数据库事务请求的时候,经常会用类似这样的代码:

1
2
3
4
5
6
7
8
9
db.begin()

try:
    # do some actions
except:
    db.rollback()
    raise
else:
    db.commit()

如果将发起事务请求的操作变成可以支持with关键字的,那么用像这样的代码就可以了:

1
2
with transaction(db):
    # do some actions

下面,详细的说明一下with的执行过程,并用两种常用的方式实现上面的代码。

with 的一般执行过程

一段基本的with表达式,其结构是这样的:

1
2
with EXPR as VAR:
    BLOCK

其中:EXPR可以是任意表达式;as VAR是可选的。其一般的执行过程是这样的:

  1. 计算EXPR,并获取一个上下文管理器。
  2. 上下文管理器的__exit()__方法被保存起来用于之后的调用。
  3. 调用上下文管理器的__enter()__方法。
  4. 如果with表达式包含as VAR,那么EXPR的返回值被赋值给VAR
  5. 执行BLOCK中的表达式。
  6. 调用上下文管理器的__exit()__方法。如果BLOCK的执行过程中发生了一个异常导致程序退出,那么异常的typevaluetraceback(即sys.exc_info()的返回值)将作为参数传递给__exit()__方法。否则,将传递三个None

将这个过程用代码表示,是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mgr = (EXPR)
exit = type(mgr).__exit__ # 这里没有执行
value = type(mgr).__enter__(mgr)
exc = True

try:
    try:
        VAR = value # 如果有 as VAR
        BLOCK
    except:
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise
finally:
    if exc:
        exit(mgr, None, None, None)

这个过程有几个细节:

  • 如果上下文管理器中没有__enter()__或者__exit()__中的任意一个方法,那么解释器会抛出一个AttributeError
  • BLOCK中发生异常后,如果__exit()__方法返回一个可被看成是True的值,那么这个异常就不会被抛出,后面的代码会继续执行。

接下来,用两种方法来实现上面来实现上面的过程的吧。

实现上下文管理器类

第一种方法是实现一个类,其含有一个实例属性db和上下文管理器所需要的方法__enter()____exit()__

1
2
3
4
5
6
7
8
9
10
11
12
class transaction(object):
    def __init__(self, db):
        self.db = db

    def __enter__(self):
        self.db.begin()

    def __exit__(self, type, value, traceback):
        if type is None:
            db.commit()
        else:
            db.rollback()

了解with的执行过程后,这个实现方式是很容易理解的。下面介绍的实现方式,其原理理解起来要复杂很多。

使用生成器装饰器

在Python的标准库中,有一个装饰器可以通过生成器获取上下文管理器。使用生成器装饰器的实现过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from contextlib import contextmanager

@contextmanager
def transaction(db):
    db.begin()

    try:
        yield db
    except:
        db.rollback()
        raise
    else:
        db.commit()

第一眼上看去,这种实现方式更为简单,但是其机制更为复杂。看一下其执行过程吧:

  1. Python解释器识别到yield关键字后,def会创建一个生成器函数替代常规的函数(在类定义之外我喜欢用函数代替方法)。
  2. 装饰器contextmanager被调用并返回一个帮助函数,这个帮助函数在被调用后会生成一个GeneratorContextManager实例。最终with表达式中的EXPR调用的是由contentmanager装饰器返回的帮助函数。
  3. with表达式调用transaction(db),实际上是调用帮助函数。帮助函数调用生成器函数,生成器函数创建一个生成器。
  4. 帮助函数将这个生成器传递给GeneratorContextManager,并创建一个GeneratorContextManager的实例对象作为上下文管理器。
  5. with表达式调用实例对象的上下文管理器的__enter()__方法。
  6. __enter()__方法中会调用这个生成器的next()方法。这时候,生成器方法会执行到yield db处停止,并将db作为next()的返回值。如果有as VAR,那么它将会被赋值给VAR
  7. with中的BLOCK被执行。
  8. BLOCK执行结束后,调用上下文管理器的__exit()__方法。__exit()__方法会再次调用生成器的next()方法。如果发生StopIteration异常,则pass
  9. 如果没有发生异常生成器方法将会执行db.commit(),否则会执行db.rollback()

再次看看上述过程的代码大致实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def contextmanager(func):
    def helper(*args, **kwargs):
        return GeneratorContextManager(func(*args, **kwargs))
    return helper

class GeneratorContextManager(object):
    def __init__(self, gen):
        self.gen = gen

    def __enter__(self):
        try:
            return self.gen.next()
        except StopIteration:
            raise RuntimeError("generator didn't yield")

    def __exit__(self, type, value, traceback):
        if type is None:
            try:
                self.gen.next()
            except StopIteration:
                pass
            else:
                raise RuntimeError("generator didn't stop")
        else:
            try:
                self.gen.throw(type, value, traceback)
                raise RuntimeError("generator didn't stop after throw()")
            except StopIteration:
                return True
            except:
                if sys.exc_info()[1] is not value:
                    raise

总结

Python的with表达式包含了很多Python特性,花点时间吃透with是一件非常值得的事情。

一些其他的例子

锁机制
1
2
3
4
5
6
7
@contextmanager
def locked(lock):
    lock.acquired()
    try:
        yield
    finally:
        lock.release()
标准输出重定向
1
2
3
4
5
6
7
8
9
10
11
12
@contextmanager
def stdout_redirect(new_stdout):
    old_stdout = sys.stdout
    sys.stdout = new_stdout
    try:
        yield
    finally:
        sys.stdout = old_stdout

with open("file.txt", "w") as f:
    with stdout_redirect(f):
        print "hello world"

引用