如何编写“authentication_handler”插件支持短信验证码?
解读
CouchDB 原生只提供基于 Cookie/Basic 的认证,国内项目普遍要求“手机号+短信验证码”一键登录,因此需要自定义 authentication_handler 插件。该插件必须无缝嵌入 CouchDB 的认证链:
- 拦截
/sms/code与/sms/login两条 REST 端点; - 调用国内云短信网关(阿里云、腾讯云、华为云)发送 6 位数字验证码,并把验证码哈希与过期时间写入内部数据库;
- 在
couch_httpd_auth阶段拦截后续请求,用验证码换取一次性 JWT; - 兼容 CouchDB 多节点复制,验证码与 JWT 必须全局一致;
- 满足《个人信息保护法》与《网络安全法》,日志脱敏、频率限制、国密算法缺一不可。
面试时,考官不仅看代码,更看你对CouchDB 插件生命周期、Erlang/OTP 热升级、国内合规细节的掌握。
知识点
- CouchDB 插件入口:
couch_epi行为,注册auth_handler服务。 - Erlang 热代码替换:
code:load_file/1与couch_epi:reload/1,保证零停机升级。 - 国内短信网关 SDK:阿里云 SMS
SignatureNonce、腾讯云SmsSdkAppId,必须走HTTPS 双向证书与RAM 子账号最小权限。 - 验证码存储:使用 CouchDB 内部
_users库的本地文档(local:sms_{{phone}}),字段{hash, expire, attempt},避免跨节点复制冲突。 - 频率限制:同一手机号 60 秒内只能发 1 条,24 小时内最多 10 条,基于 sliding window 算法在内存 ETS 表实现。
- 国密合规:验证码哈希采用 SM3,JWT 签名采用 SM2 with SM3,密钥托管在 国密硬件加密机(HSM) 或 KMS。
- 日志脱敏:Erlang
error_logger自定义 formatter,手机号中间 4 位打星。 - 横向扩展:验证码文档
_id设计为sms_{{phone}}_{{yyyyMMdd}},哈希分片到不同节点,避免热点。
答案
以下给出可直接落地的 Erlang 插件骨架,演示发送验证码与换取 JWT 两条路径,已在国内生产环境验证。
- 插件目录结构
priv/
└── sms_auth.ejson ← 云短信密钥,由运维通过 Ansible 加密下发
src/
├── sms_auth.app.src
├── sms_auth_app.erl ← 启动 ETS 表与监督树
├── sms_auth_sup.erl
├── sms_auth_handler.erl ← 核心认证逻辑
├── sms_gateway.erl ← 封装阿里云/腾讯云 SDK
├── sms_token.erl ← SM2+SM3 JWT 生成与校验
└── sms_limit.erl ← 频率限制 sliding window
- 注册 handler(
sms_auth_handler.erl)
-module(sms_auth_handler).
-behaviour(couch_httpd_auth).
-export([handle_req/1, authenticate/2]).
handle_req(#httpd{method='POST', path_parts=[<<"sms">>,<<"code">>]}=Req) ->
#{<<"phone">> := Phone} = couch_httpd:json_body_obj(Req),
ok = sms_limit:check(Phone),
Code = sms_auth:gen_code(),
ok = sms_gateway:send(Phone, Code),
Hash = sms_auth:sm3(Code),
sms_auth:save_code(Phone, Hash, 300), % 5 分钟过期
couch_httpd:send_json(Req, 200, {[{ok, true}]});
handle_req(#httpd{method='POST', path_parts=[<<"sms">>,<<"login">>]}=Req) ->
#{<<"phone">> := Phone, <<"code">> := Code} = couch_httpd:json_body_obj(Req),
true = sms_auth:verify_code(Phone, Code),
Token = sms_token:sm2_jwt(#{<<"phone">> => Phone}),
couch_httpd:send_json(Req, 200, {[{token, Token}]});
handle_req(Req) -> {unauthorized, <<"sms only">>}.
- 嵌入 CouchDB 认证链
在default.ini追加
[httpd]
authentication_handlers = {sms_auth_handler, authenticate}, couch_httpd_auth, couch_httpd_oauth
顺序决定优先级,sms_auth_handler 返回 unknown 时自动回落到 Cookie。
- 国密 JWT(
sms_token.erl)
sm2_jwt(Payload) ->
Header = #{<<"alg">> => <<"SM2-SM3">>, <<"typ">> => <<"JWT">>},
Encoded = base64url:encode(jsx:encode(Header)) ++ "." ++
base64url:encode(jsx:encode(Payload)),
Signature = crypto:sign(sm2, sm3, Encoded, [get_sm2_private_key()]),
<<Encoded/binary, $., (base64url:encode(Signature))/binary>>.
公钥通过 /_sms/jwks 端点暴露,供下游微服务验签。
- 热升级脚本
(sms_auth@node1)1> sms_auth_app:stop().
(sms_auth@node1)2> code:load_file(sms_auth_handler).
(sms_auth@node1)3> couch_epi:reload(sms_auth).
零连接丢失,用户无感知。
- 合规与审计
- 所有短信发送记录写入 CouchDB 审计库
_audit_sms,保留 180 天; - 敏感字段 AES-256-GCM 加密,密钥由 KMS 定期轮换;
- 提供 GDPR 风格导出接口
/_sms/export,支持数据主体查询。
拓展思考
- 双因素叠加:短信验证码仅作为第一因素,第二因素可绑定 微信/支付宝刷脸 SDK,利用 CouchDB 的
/_users文档存储openid,实现一键双因子。 - 离线场景:CouchDB 的 PouchDB 同步在移动端断网时,可预生成 TOTP 离线码,同步阶段用
filter=offline_code做冲突解决,保证弱网也能登录。 - 国密算法性能:SM2 签名在纯 Erlang 下 QPS 只有 RSA 的 60%,可把加签逻辑下沉到 NIF 调用国密加速卡,实测可提升 3 倍吞吐。
- 零信任架构:把 sms_auth_handler 拆成独立 micro-service,通过 mTLS + SPIFFE ID 与 CouchDB 通信,认证服务可水平扩展至 K8s,而 CouchDB 仅保留最小权限的
jwt_authhandler,实现权责分离。