Riak作为分布式键值存储,被不少团队用作高可用后端。但它自身并没有一套完整的用户身份体系,官方提供的Riak Security模块仅支持基于证书的ACL与用户密码,无法直接对接现代系统的第三方登录。OpenID Connect(简称OIDC)是在OAuth 2.0之上建立的标准身份层,通过IdP签发的JWT让应用确认用户身份。将二者结合,核心思路不是改造Riak源码,而是在请求入口完成令牌校验,再把可信上下文交给Riak。

为什么Riak原生不支持OpenID Connect
Riak的设计目标是线性扩展与最终一致性,其安全模型停留在网络层与基础认证。开启Riak Security后,管理员通过riak-admin security enable激活,随后用riak-admin security add-user建立本地账号,这些账号的密码以哈希存于集群内。它既不发起对外部IdP的调用,也不解析JWT这样的Bearer令牌。换句话说,Riak信任的是“谁连上来”,而不是“连上来的人声称是谁”。
当我们把Riak直接绑在公网或内部服务总线时,如果仅靠IP白名单,一旦网关被绕开,任何能拿到证书的人都能以最高权限操作桶。OpenID Connect解决的是跨系统身份互信:终端用户登录Auth0后拿到id_token,这个JWT包含sub、aud等声明,由IdP私钥签名。Riak无法验证这个签名,所以必须在它前面放一个能验签的组件。
有些团队尝试在Riak的自定义钩子(bucket hook)里写Erlang代码去访问IdP,这会带来两个问题:一是钩子执行会阻塞写入路径,拖慢整体吞吐;二是Erlang里维护HTTP客户端与JWKS缓存复杂度高,升级Riak时钩子易失效。因此更合理的架构是在边界做统一校验。
用反向代理完成OIDC令牌校验的实践
最常见的方案是在Riak前部署Nginx或Envoy,由代理负责OpenID Connect流程。以Nginx为例,可使用auth_request指令把每个对Riak的请求先转发到校验服务,只有携带合法Authorization: Bearer头且JWT验签通过时,才把请求代理到Riak后端。这样Riak完全不用感知OIDC存在,只需接收来自内网代理的已认证请求。
校验服务可以是一个轻量Go程序,启动时拉取IdP的JWKS(JSON Web Key Set),缓存公钥,然后暴露/validate接口。下面是一段简单的校验逻辑示例,演示如何解析并验证JWT的受众与签名:
package main
import (
"fmt"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
)
// 假设已从IdP获取并缓存的公钥
var publicKey = loadPublicKeyFromJWKS()
func validateToken(tokenStr string) (jwt.MapClaims, error) {
// 解析并验签,同时检查aud与exp
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return publicKey, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
// 确认受众是本系统
if claims["aud"] != "riak-gateway" {
return nil, fmt.Errorf("wrong audience")
}
return claims, nil
}
func authHandler(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
return
}
if _, err := validateToken(strings.TrimPrefix(auth, "Bearer ")); err != nil {
w.WriteHeader(http.StatusForbidden)
return
}
w.WriteHeader(http.StatusOK)
}
在Nginx配置中,我们把/validate作为内部 location,并用auth_request /validate;保护Riak的 location。这种结构的优势是校验逻辑与存储解耦,IdP轮换密钥时只需更新Go程序缓存,Riak节点无需重启。同时代理可把JWT里的sub写入请求头如X-User-Id传给Riak,便于后续审计日志关联。
不过要注意,Bearer令牌不能无期限透传。代理应限制自身到Riak的连接使用mTLS,避免内部嗅探。另外如果Riak启用了多租户桶命名,代理需依据sub做桶前缀映射,防止A用户通过构造URL访问B用户的桶。这部分规则应在代理层用Lua或外部授权服务强制。
应用层直连Riak与网关校验的对比
另一种思路是让业务应用先完成OIDC登录,拿到JWT后,应用在调用Riak客户端(如riak-go-client)之前自行验签,然后把用户上下文通过Riak的自定义HTTP头或对象元数据带进去。这种方式把校验分散到每个调用方,适合无法统一部署网关的碎片化环境。
对比来看,网关模式的优点在于策略集中、升级IdP配置不影响业务代码,且能统一限流。缺点是多一跳延迟,并且网关成为单点,需要部署高可用。应用层模式的优点是与现有微服务鉴权一致,Riak仍当作纯数据库;缺点是每个语言SDK都要实现一遍JWKS刷新,容易出现某服务漏验aud的漏洞。下面的表格列出了核心差异:
| 维度 | 反向代理网关校验 | 应用层直连校验 |
|---|---|---|
| 身份校验位置 | 边界代理 | 各业务进程 |
| Riak改动量 | 零改动 | 零改动,但需约定头字段 |
| 密钥更新影响 | 仅网关重启 | 所有服务重发版 |
| 越权风险点 | 代理映射规则错误 | 某应用跳过验签 |
从运维角度,如果团队已有API网关,直接复用其OIDC插件(如Kong的openid-connect插件)是最省力的。若Riak只在内网被少量可信服务访问,应用层校验也能满足合规。但无论如何,绝不能把Riak的HTTP端口直接暴露给携带JWT的终端,因为Riak读不到JWT也就无法拒绝非法sub,等于任何人都能读写。
最后补充一个常见误区:有人以为在Riak里存一个“用户表”并用OIDC的sub当key,就完成了身份认证。其实存储用户资料不等于鉴权,攻击者可以不走IdP而直接用Riak协议写数据伪造记录。只有请求入口的密码学校验,才能确保sub真实来自IdP。理清这一边界,Riak加OpenID Connect的架构才算落地。
RiakOpenID_Connect身份认证修改时间:2026-08-16 22:54:19