【问题标题】:How to extract CN from X509Certificate in Java?如何从 Java 中的 X509Certificate 中提取 CN?
【发布时间】:2011-02-24 06:21:05
【问题描述】:

我正在使用SslServerSocket 和客户端证书,并希望从客户端的X509Certificate 的SubjectDN 中提取CN。

目前我打电话给cert.getSubjectX500Principal().getName(),但这当然给了我客户的总格式化DN。出于某种原因,我只是对 DN 的 CN=theclient 部分感兴趣。有没有办法在不自己解析字符串的情况下提取这部分 DN?

【问题讨论】:

标签: java ssl x509certificate x509


【解决方案1】:

以下是新的未弃用的 BouncyCastle API 的一些代码。您需要 bcmail 和 bcprov 分发版。

X509Certificate cert = ...;

X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
RDN cn = x500name.getRDNs(BCStyle.CN)[0];

return IETFUtils.valueToString(cn.getFirst().getValue());

【讨论】:

  • @grak,我很想知道你是如何找到这个解决方案的。当然,仅仅通过查看 API 文档,我永远无法弄清楚这一点。
  • 是的,我同意这种观点......我不得不在邮件列表中询问。
  • 请注意,当前(2012 年 10 月 23 日)BouncyCastle (1.47) 上的此代码也需要 bcpkix 分发。
  • 一个证书可以有多个CN。而不是仅仅返回 cn.getFirst() 你应该遍历所有并返回一个 CN 列表。
  • IETFUtils.valueToString 似乎没有产生正确的结果。由于 base 64 编码(例如AAECAwQFBgcICQoLDA0ODw==),我有一个包含一些等号的 CN。 valueToString 方法在结果中添加反斜杠。取而代之的是,使用toString 似乎有效。很难确定这实际上是对 api 的正确使用。
【解决方案2】:

这是另一种方式。这个想法是您获得的 DN 是 rfc2253 格式,这与用于 LDAP DN 的格式相同。那么为什么不重用 LDAP API?

import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;

String dn = x509cert.getSubjectX500Principal().getName();
LdapName ldapDN = new LdapName(dn);
for(Rdn rdn: ldapDN.getRdns()) {
    System.out.println(rdn.getType() + " -> " + rdn.getValue());
}

【讨论】:

  • 如果你使用 spring 的一个有用的快捷方式: LdapUtils.getStringValue(ldapDN, "cn");
  • 至少对于我在 CN 上工作的情况是 within 一个多属性 RDN。换句话说:建议的解决方案不会迭代 RDN 的属性。应该!
  • String commonName = new LdapName(certificate.getSubjectX500Principal().getName()).getRdns().stream() .filter(i -> i.getType().equalsIgnoreCase("CN")).findFirst().get().getValue().toString();
  • 注意:虽然它看起来是一个很好的解决方案,但它也存在一些问题。我使用这个已经好几年了,直到我发现“非标准”字段的解码问题。对于具有诸如CN(又名2.5.4.3)之类的众所周知类型的字段,Rdn#getValue() 包含String。但是,对于自定义类型,结果是byte[](可能基于以# 开头的内部编码表示)。 Ofc,byte[] -> String 是可能的,但包含额外的(不可预测的)字符。我已经使用基于 BC 的 @laz 解决方案解决了这个问题,因为它在 String 中正确处理和解码。
【解决方案3】:

如果添加依赖项不是问题,您可以使用 Bouncy Castle's API 来处理 X.509 证书:

import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.X509Principal;

...

final X509Principal principal = PrincipalUtil.getSubjectX509Principal(cert);
final Vector<?> values = principal.getValues(X509Name.CN);
final String cn = (String) values.get(0);

更新

在发布此帖子时,这是执行此操作的方法。然而,正如 gtrak 在 cmets 中提到的那样,这种方法现在已被弃用。请参阅使用新的 Bouncy Castle API 的 gtrak 的 updated code

【讨论】:

  • 似乎 X509Name 在 Bouncycastle 1.46 中已被弃用,他们打算使用 x500Name。对此或做同样事情的预期替代方案有任何了解吗?
  • 哇,看看新的 API,我很难弄清楚如何实现与上述代码相同的目标。也许 Bouncycastle 邮件列表档案可能有答案。如果我弄明白了,我会更新这个答案。
  • 我遇到了同样的问题。如果您有任何想法,请告诉我。据我所知:x500name = X500Name.getInstance(PrincipalUtil.getIssuerX509Principal(cert)); RDN cn = x500name.getRDNs(BCStyle.CN)[0];
  • 我通过邮件列表讨论找到了如何做到这一点,我创建了一个显示如何做到的答案。
  • 很好找 gtrak。我花了 10 分钟试图弄清楚这一点,但再也没有回过头来。
【解决方案4】:

作为不需要 ''bcmail'' 的 gtrak 代码的替代方案:

    X509Certificate cert = ...;
    X500Principal principal = cert.getSubjectX500Principal();

    X500Name x500name = new X500Name( principal.getName() );
    RDN cn = x500name.getRDNs(BCStyle.CN)[0]);

    return IETFUtils.valueToString(cn.getFirst().getValue());

@Jakub:在我的软件必须在 Android 上运行之前,我一直使用你的解决方案。而且Android没有实现javax.naming.ldap :-(

【讨论】:

  • 这正是我提出这个解决方案的原因:移植到 Android...
  • 不确定何时更改,但现在可以使用:X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(使用 java 8)
  • IETFUtils.valueToString转义 形式返回值。我发现只是调用 .toString() 而不是为我工作。
【解决方案5】:

http://www.cryptacular.org一行

CertUtil.subjectCN(certificate);

Java 文档: http://www.cryptacular.org/javadocs/org/cryptacular/util/CertUtil.html#subjectCN(java.security.cert.X509Certificate)

Maven 依赖:

<dependency>
    <groupId>org.cryptacular</groupId>
    <artifactId>cryptacular</artifactId>
    <version>1.1.0</version>
</dependency>

【讨论】:

  • 请注意,Cryptacular 1.1.x 系列适用于 Java 7,而 1.2.x 系列适用于 Java 8。不过是非常好的库!
【解决方案6】:

到目前为止发布的所有答案都有一些问题:大多数使用内部 X500Name 或外部 Bounty Castle 依赖项。以下内容基于@Jakub 的答案,仅使用公共 JDK API,但也按照 OP 的要求提取 CN。它还使用 Java 8,它在 2017 年中期,你真的应该这样做。

Stream.of(certificate)
    .map(cert -> cert.getSubjectX500Principal().getName())
    .flatMap(name -> {
        try {
            return new LdapName(name).getRdns().stream()
                    .filter(rdn -> rdn.getType().equalsIgnoreCase("cn"))
                    .map(rdn -> rdn.getValue().toString());
        } catch (InvalidNameException e) {
            log.warn("Failed to get certificate CN.", e);
            return Stream.empty();
        }
    })
    .collect(joining(", "))

【讨论】:

  • 在我的例子中,CN 是 within 一个多属性 RDN。我认为您需要增强此解决方案,以便对于每个 RDN,您将迭代 RDN 属性,而不是只查看 RDN 的第一个属性,我认为这就是您在此处隐式执行的操作。
【解决方案7】:

如果您不想依赖 BouncyCastle,以下是使用正则表达式而不是 cert.getSubjectX500Principal().getName() 的方法。

此正则表达式将解析一个可分辨名称,为每个匹配项提供 nameval 一个捕获组。

当 DN 字符串包含逗号时,它们应被引用 - 此正则表达式正确处理带引号和不带引号的字符串,还可以处理带引号的字符串中的转义引号:

(?:^|,\s?)(?:(?&lt;name&gt;[A-Z]+)=(?&lt;val&gt;"(?:[^"]|"")+"|[^,]+))+

这里的格式很好:

(?:^|,\s?)
(?:
    (?<name>[A-Z]+)=
    (?<val>"(?:[^"]|"")+"|[^,]+)
)+

这是一个链接,您可以查看它的实际效果: https://regex101.com/r/zfZX3f/2

如果你想让一个正则表达式获得 CN,那么这个改编版本就可以了:

(?:^|,\s?)(?:CN=(?&lt;val&gt;"(?:[^"]|"")+"|[^,]+))

【讨论】:

  • 最可靠的答案。此外,如果您想支持由其编号指定的 OID(例如 OID.2.5.4.97),则允许的字符应从 [A-Z] 扩展到 [A-Z,0-9,.]
【解决方案8】:

我有 BouncyCastle 1.49,它现在拥有的类是 org.bouncycastle.asn1.x509.Certificate。我查看了IETFUtils.valueToString() 的代码——它正在用反斜杠进行一些花哨的转义。对于一个域名它不会做任何坏事,但我觉得我们可以做得更好。在我查看过的情况下,cn.getFirst().getValue() 返回不同类型的字符串,它们都实现了 ASN1String 接口,该接口提供了一个 getString() 方法。所以,似乎对我有用的是

Certificate c = ...;
RDN cn = c.getSubject().getRDNs(BCStyle.CN)[0];
return ((ASN1String)cn.getFirst().getValue()).getString();

【讨论】:

  • 我遇到了反斜杠问题,所以这解决了我的问题。
【解决方案9】:

更新:这个类在“sun”包中,你应该谨慎使用它。感谢埃米尔的评论:)

只是想分享,想要获得CN,我愿意:

X500Name.asX500Name(cert.getSubjectX500Principal()).getCommonName();

关于 Emil Lundberg 的评论见:Why Developers Should Not Write Programs That Call 'sun' Packages

【讨论】:

  • 这是当前答案中我最喜欢的,因为它简单、易读并且仅使用 JDK 中捆绑的内容。
  • 同意你所说的使用JDK类:)
  • 然而,应该注意的是,javac 警告 X500Name 是一个内部专有 API,可能会在未来的版本中被删除。
  • 是的,在阅读了linked FAQ 之后,我需要撤销我的第一条评论。对不起。
  • 完全没问题。你指出的真的很重要。谢谢 :) 事实上,我不再使用那个类了 :P
【解决方案10】:

确实,感谢gtrak,看来要获得客户端证书并提取 CN,这很可能有效。

    X509Certificate[] certs = (X509Certificate[]) httpServletRequest
        .getAttribute("javax.servlet.request.X509Certificate");
    X509Certificate cert = certs[0];
    X509CertificateHolder x509CertificateHolder = new X509CertificateHolder(cert.getEncoded());
    X500Name x500Name = x509CertificateHolder.getSubject();
    RDN[] rdns = x500Name.getRDNs(BCStyle.CN);
    RDN rdn = rdns[0];
    String name = IETFUtils.valueToString(rdn.getFirst().getValue());
    return name;

【讨论】:

【解决方案11】:

使用纯 Java 的另一种方法:

public static String getCommonName(X509Certificate certificate) {
    String name = certificate.getSubjectX500Principal().getName();
    int start = name.indexOf("CN=");
    int end = name.indexOf(",", start);
    if (end == -1) {
        end = name.length();
    }
    return name.substring(start + 3, end);
}

【讨论】:

    【解决方案12】:

    在不使用任何库的情况下获取证书的通用名称。使用正则表达式

    获取名字

    String name = x509Certificate.getSubjectDN().getName();
    

    从全名中提取通用名

        String name = "CN=Go Daddy Root Certificate Authority - G2, O=\"GoDaddy.com, Inc.\", L=Scottsdale, ST=Arizona, C=US";
        Pattern pattern = Pattern.compile("CN=(.*?)(?:,|\$)");
        Matcher matcher = pattern.matcher(name);
        if (matcher.find()) {
            System.out.println(matcher.group(1));
        }
    

    希望这对任何人都有帮助。(-_-)

    【讨论】:

    • 由于简单而被赞成。但是,正则表达式不应在美元符号前有反斜杠。
    【解决方案13】:

    可以使用 cryptacular,它是一个构建在 bouncycastle 之上的 Java 密码库,以便于使用。

    RDNSequence dn = new NameReader(cert).readSubject();
    return dn.getValue(StandardAttributeType.CommonName);
    

    【讨论】:

    • 最好使用@Erdem Memisyazici 的建议。
    【解决方案14】:

    从证书中获取 CN 并不是那么简单。下面的代码一定会对你有所帮助。

    String certificateURL = "C://XYZ.cer";      //just pass location
    
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    X509Certificate testCertificate = (X509Certificate)cf.generateCertificate(new FileInputStream(certificateURL));
    String certificateName = X500Name.asX500Name((new X509CertImpl(testCertificate.getEncoded()).getSubjectX500Principal())).getCommonName();
    

    【讨论】:

    • 不。 X500Name 是 JDK 内部类。
    【解决方案15】:

    BC 使提取变得更加容易:

    X500Principal principal = x509Certificate.getSubjectX500Principal();
    X500Name x500name = new X500Name(principal.getName());
    String cn = x500name.getCommonName();
    

    【讨论】:

    • 我在X500Name 中找不到任何.getCommonName() 方法。
    • (@lapo) 你确定你实际上没有使用sun.security.x509.X500Name - 正如几年前提到的其他答案没有记录并且不能依赖?
    • 嗯,我确实链接了org.bouncycastle.asn1.x500.X500Name 类的JavaDoc,它没有显示该方法……
    【解决方案16】:

    正则表达式使用起来相当昂贵。对于这样一个简单的任务,它可能是一个过度杀戮。相反,您可以使用简单的字符串拆分:

    String dn = ((X509Certificate) certificate).getIssuerDN().getName();
    String CN = getValByAttributeTypeFromIssuerDN(dn,"CN=");
    
    private String getValByAttributeTypeFromIssuerDN(String dn, String attributeType)
    {
        String[] dnSplits = dn.split(","); 
        for (String dnSplit : dnSplits) 
        {
            if (dnSplit.contains(attributeType)) 
            {
                String[] cnSplits = dnSplit.trim().split("=");
                if(cnSplits[1]!= null)
                {
                    return cnSplits[1].trim();
                }
            }
        }
        return "";
    }
    

    【讨论】:

    • 我真的很喜欢!平台和库独立。这真的很酷!
    • 对我投反对票。如果您阅读RFC 2253,您会发现必须考虑一些极端情况,例如转义逗号 \, 或引用的值。
    【解决方案17】:

    X500Name 是 JDK 的内部实现,但是你可以使用反射。

    public String getCN(String formatedDN) throws Exception{
        Class<?> x500NameClzz = Class.forName("sun.security.x509.X500Name");
        Constructor<?> constructor = x500NameClzz.getConstructor(String.class);
        Object x500NameInst = constructor.newInstance(formatedDN);
        Method method = x500NameClzz.getMethod("getCommonName", null);
        return (String)method.invoke(x500NameInst, null);
    }
    

    【讨论】:

      【解决方案18】:

      您可以尝试使用getName(X500Principal.RFC2253, oidMap)getName(X500Principal.CANONICAL, oidMap) 来查看哪种格式的DN 字符串最好。也许oidMap 映射值之一将是您想要的字符串。

      【讨论】:

        【解决方案19】:

        对于多值属性 - 使用 LDAP API ...

                X509Certificate testCertificate = ....
        
                X500Principal principal = testCertificate.getSubjectX500Principal(); // return subject DN
                String dn = null;
                if (principal != null)
                {
                    String value = principal.getName(); // return String representation of DN in RFC 2253
                    if (value != null && value.length() > 0)
                    {
                        dn = value;
                    }
                }
        
                if (dn != null)
                {
                    LdapName ldapDN = new LdapName(dn);
                    for (Rdn rdn : ldapDN.getRdns())
                    {
                        Attributes attributes = rdn != null
                            ? rdn.toAttributes()
                            : null;
        
                        Attribute attribute = attributes != null
                            ? attributes.get("CN")
                            : null;
                        if (attribute != null)
                        {
                            NamingEnumeration<?> values = attribute.getAll();
                            while (values != null && values.hasMoreElements())
                            {
                                Object o = values.next();
                                if (o != null && o instanceof String)
                                {
                                    String cnValue = (String) o;
                                }
                            }
                        }
                    }
                }
        

        【讨论】:

          【解决方案20】:

          使用 Spring Security 可以使用 SubjectDnX509PrincipalExtractor:

          X509Certificate certificate = ...;
          new SubjectDnX509PrincipalExtractor().extractPrincipal(certificate).toString();
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-01-14
            • 1970-01-01
            • 2011-11-23
            • 1970-01-01
            相关资源
            最近更新 更多