加载中...
<cover></cover>

# 阿里云金融级实人认证接入踩坑记

2023-12-02 技术向

本文最后更新于 天前，文中部分描述可能已经过时。

最近需要给一个基于 Django 的项目开发实名认证功能，除了常规的核验姓名和身份证号是否匹配，还需要对用户进行活体检测。看了一圈最后选定了阿里云的金融级实人认证产品，可以直接让用户使用支付宝APP完成活体检测的认证过程，开发工作量相对较小，对用户而言也比较方便（毕竟这年头谁手机上还没个支付宝呢）。

开通金融级实人认证后，我开始照着阿里云提供的开发参考文档尝试将其接入到项目中。得益于阿里云这份含糊其辞、不清不楚的过时文档，开发花费的时间比我想象中要多😇，在此也记录一下我踩过的坑，希望能够帮助到后来者。

## TL;DR

- 阿里云文档中的 Python SDK 版本过时，需要手动指定版本号
- 阿里云文档中提供的国密 SM2公钥为压缩格式，需要先还原完整公钥
- 阿里云使用 C1C2C3模式，而非现行标准的 C1C3C2模式
- 须确保加密结果的第一个字节为 `\x04`，否则须手动补充

## 过时的 SDK 版本

因为项目是基于 Django 框架开发的，所以我参考了文档中 Python SDK 的部分。按文档所说，我首先通过 `pip install aliyun-python-sdk-saf` 安装了云产品SAF SDK，接着在项目中引入 SDK 包时却出现了问题。

怀疑八成是文档没及时更新，看了眼包安装目录下的文件结构，果然最新版本都已经 `v20190521` 了，改一下版本号就解决了问题。

同时最好在 `requirements.txt` 中锁版本，以免以后哪次 `pip install` 时更新了 SDK 导致又出现问题。

## 加密传参全靠猜

由于项目的合规要求，数据库不能留存用户的身份证号明文或可解密的密文，而阿里云的金融级实人认证接口刚好支持非对称加密传参，因此可以将用户输入的身份证号使用阿里云提供的公钥进行非对称加密，然后存储在数据库中（由于私钥由阿里云保管，即使数据库发生数据泄露也无法解开），发起认证请求时直接将密文传递给阿里云。

然而文档中关于加密方式的约定含糊其辞，只提到了加密方式为国密 SM2，给出了一个公钥，并提供了一段 Java 语言下的调包例程。

好在 Python 这边也已经有现成的国密加密包了，我天真地以为调用一下包里的加密函数就行了，于是我一开始是这么写的：

    import base64
    from gmssl import sm2

    sm2_crypt = sm2.CryptSM2(public_key='02cd77e007bdc86eeaf9a479ba7a2c22bc0a517ccb3a6975c3f94b4ac93347dea6', private_key='', mode=1)
    idcard_encrypted = base64.b64encode(sm2_crypt.encrypt(idcard.encode('utf-8'))).decode('utf-8')

结果呢，还没到给阿里云传参这一步，光是加密就报错了：

    TypeError: object of type 'NoneType' has no len()

翻了翻 Issues 才知道存在公钥压缩这回事。根据现行 GB/T 35276-2017 7.1节的定义，SM2算法公钥内容为 `04||X||Y`，其中 X 和 Y 分别标识公钥的 x 分量和 y 分量，其长度各为256位。阿里云提供的公钥既不以 `04` 开头，长度也不满足规范，显然是经过压缩的，需要先还原完整公钥。可以使用这个小工具进行还原，也可以研究一下压缩的原理然后自己造个轮子，当然我懒所以选择前者。

    import base64
    from gmssl import sm2

    sm2_crypt = sm2.CryptSM2(public_key='04cd77e007bdc86eeaf9a479ba7a2c22bc0a517ccb3a6975c3f94b4ac93347dea65fb8709f2915105fdd5c81bae765774ca7a9392ad3b557b1d239741c2899c868', private_key='', mode=1)
    idcard_encrypted = base64.b64encode(sm2_crypt.encrypt(idcard.encode('utf-8'))).decode('utf-8')

这样就可以正常加密了。正以为万事大吉，当我把加密的密文传给阿里云时，阿里云接口却报错了。令人无语的是，接口返回的信息只有一句 `501 系统错误`，除此之外啥也没有。起初我甚至没有怀疑是加密的问题，还以为是别的参数有问题或者阿里云接口挂了（毕竟最近阿里云频繁出事），直到我尝试明文传参成功后才意识到问题出在加密上。显然，阿里云的私钥解不开我传递给它的密文。

于是我又花了大量的时间弄清究竟是哪出了问题。根据现行 GB/T 35276-2017 7.2节的定义，我们不妨将 x 分量和 y 分量合称为 C1，密文称为 C2，杂凑值称为 C3，则加密数据由 C1、C2、C3三部分组成，且三者的排列顺序为 C1C3C2。同时在查阅了一些资料后，我还了解到在最初的国密标准中，加密数据的排列顺序为 C1C2C3，不过我没有找到相关的标准文件。正因为国密 SM2存在 C1C3C2和 C1C2C3两种加密模式，我使用的 Python 国密加密包提供了 `mode` 选项以便开发者根据需求设定加密模式。然而阿里云根本没告诉我它解密时使用的是哪一种，不过这个问题暂时不重要，因为经过尝试无论哪一种都问题依旧。

迫不得已我只好把目光看向了阿里云提供的 Java 例程，尝试跑了一下，比对了同一个字符串使用 Python 国密加密包和使用 Java 例程的加密结果，终于发现了问题所在。Java 例程产生的密文 Base64 串永远以字符 `B` 开头，而 Python 加密包产生的密文 Base64 串开头字符却一直在变，这显然不合理。通过观察 Java 例程加密结果的第一个字节，我惊奇地发现竟然永远是 `\x04` 这个熟悉的家伙。于是我手动给 Python 加密包的加密结果加上了这个字节，然后尝试了一下 C1C3C2和 C1C2C3两种加密模式，确认是 C1C2C3 模式（非现行标准），总算是对接成功了。

    import base64
    from gmssl import sm2

    sm2_crypt = sm2.CryptSM2(public_key='04cd77e007bdc86eeaf9a479ba7a2c22bc0a517ccb3a6975c3f94b4ac93347dea65fb8709f2915105fdd5c81bae765774ca7a9392ad3b557b1d239741c2899c868', private_key='', mode=0)
    idcard_encrypted = base64.b64encode(b'\x04' + sm2_crypt.encrypt(idcard.encode('utf-8'))).decode('utf-8')

## 总结

这次开发可以说是一波三折，一部分原因是对国密算法不熟悉，互联网上相关的资料（尤其是 Python 下的进行国密加密）也较少，另一部分原因则是阿里云存在如下的问题有待改进：

- 文档过时内容未及时更新
- 接口不返回详细错误信息，导致排查困难
- 参数加密约定不清晰、不详细
- 使用非现行标准（C1C2C3）且不加说明

总之希望这篇文章能够帮助到后来者，也希望阿里云能够改进文档和接口，提升产品的易用性。

阿里云金融级实人认证接入踩坑记
https://blog.hans362.cn/post/aliyun-financial-grade-id-verification/
本文作者
Hans362
最后更新
2023-12-02
许可协议
CC BY-NC-SA 4.0
转载或引用本文时请遵守许可协议，注明出处、不得用于商业用途！
喜欢这篇文章吗？考虑支持一下作者吧～
爱发电 支付宝