You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Keycloak v26如何从X509证书UPN中提取用户名?

Keycloak v26 从客户端证书UPN提取用户名解决方案

问题背景

配置Keycloak(v26)X509认证流程时,需从客户端证书OtherName字段(UPN + RFC822格式,值如username@active.directory.domain)提取仅含username的部分用于用户认证。当前认证流程中的「X509/Validate Username Form config」步骤,其「nom principal」字段使用完整UPN值,与系统中仅存username的用户账号不匹配,导致认证失败。

已知限制:

  • Keycloak界面提供的正则选项对「Subject's Alternative Name otherName (UPN)」类型的身份源无效
  • 证书DN中不包含目标用户名,UPN是唯一存储该信息的位置

已执行排查步骤

  • 开启TRACE日志,确认BCUserIdentityExtractorProvider读取到完整UPN值,用户因user_not_found错误被拒绝
  • 测试多种正则表达式,均无法对UPN类型身份源生效
  • 尝试通过X509字段映射用户名属性,未找到可行配置

可行解决方案

方法1:自定义UserIdentityExtractorProvider扩展

Keycloak默认的BCUserIdentityExtractorProvider不支持对UPN应用正则提取,需自定义扩展实现提取逻辑:

  1. 创建Java类继承AbstractUserIdentityExtractorProvider,重写extract方法,从X509Certificate中解析UPN值,通过字符串分割或正则提取用户名部分
  2. 将打包后的JAR文件放入Keycloak的providers目录
  3. 在Keycloak管理界面中,进入「Authentication」→「Bindings」→「X509 User Identity Extractor」,选择自定义的提取器

核心代码片段示例:

@Override
public UserIdentity extract(X509Certificate certificate, Map<String, String> config) {
    String upn = extractUPNFromCertificate(certificate);
    if (upn != null && upn.contains("@")) {
        String username = upn.split("@")[0];
        return UserIdentity.builder()
                .username(username)
                .build();
    }
    return super.extract(certificate, config);
}

private String extractUPNFromCertificate(X509Certificate cert) {
    // 借助BouncyCastle库解析证书OtherName字段中的UPN值
    // 实现ASN.1格式解析逻辑,提取对应OID(1.3.6.1.4.1.311.20.2.3)的UPN内容
}

方法2:使用X509认证流程的脚本扩展

通过Keycloak的脚本功能,在认证流程中添加「Script Authenticator」步骤,覆盖用户名提取逻辑:

  1. 在Keycloak管理界面中,进入「Authentication」→「Flows」,复制默认的「X509/Validate Username Form」流程
  2. 在复制后的流程中,添加「Script Authenticator」步骤,置于「X509/Validate Username Form config」之前
  3. 编写Groovy脚本,从认证上下文获取客户端证书,解析UPN并提取用户名,存入认证上下文的用户名字段

示例脚本:

import org.keycloak.authentication.AuthenticationFlowContext
import java.security.cert.X509Certificate
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.DERUTF8String
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralNames

def context = context as AuthenticationFlowContext
X509Certificate cert = context.getHttpRequest().getAttribute("javax.servlet.request.X509Certificate") as X509Certificate

if (cert != null) {
    byte[] sanBytes = cert.getExtensionValue("2.5.29.17")
    if (sanBytes != null) {
        ASN1InputStream asn1InputStream = new ASN1InputStream(sanBytes)
        def san = GeneralNames.getInstance(asn1InputStream.readObject())
        for (GeneralName name : san.getNames()) {
            if (name.getTagNo() == GeneralName.otherName) {
                def otherName = name.getName()
                def oid = ASN1ObjectIdentifier.getInstance(otherName.getObjectAt(0))
                if (oid.getId().equals("1.3.6.1.4.1.311.20.2.3")) { // UPN对应的OID
                    def upnValue = DERUTF8String.getInstance(otherName.getObjectAt(1)).getString()
                    if (upnValue.contains("@")) {
                        String username = upnValue.split("@")[0]
                        def targetUser = context.session.users().getUserByUsername(username, context.realm())
                        if (targetUser != null) {
                            context.setUser(targetUser)
                            context.success()
                            return
                        }
                    }
                }
            }
        }
    }
}
context.failure(context.errorEvent("user_not_found"))

方法3:修改Keycloak默认实现(不推荐)

若无法自定义扩展,可修改Keycloak默认的BCUserIdentityExtractorProvider代码,添加UPN正则提取逻辑后重新编译打包。但此方法会导致后续版本升级困难,仅作为临时应急方案。

内容的提问来源于stack exchange,提问作者Paul Evans

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 12:12:19