我正在使用SslServerSocket
和客户端证书,并希望从客户端的SubjectDN中提取CN X509Certificate
。
目前,我打电话,cert.getSubjectX500Principal().getName()
但这当然给了我客户端的总格式化DN。由于某种原因,我只对CN=theclient
DN感兴趣。有没有一种方法可以提取DN的这一部分而无需自己解析String?
我正在使用SslServerSocket
和客户端证书,并希望从客户端的SubjectDN中提取CN X509Certificate
。
目前,我打电话,cert.getSubjectX500Principal().getName()
但这当然给了我客户端的总格式化DN。由于某种原因,我只对CN=theclient
DN感兴趣。有没有一种方法可以提取DN的这一部分而无需自己解析String?
Answers:
这是新的不推荐使用的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());
IETFUtils.valueToString
不会出现产生正确的结果。由于使用base 64编码(例如AAECAwQFBgcICQoLDA0ODw==
),我有一个包含一些等号的CN 。该valueToString
方法将反斜杠添加到结果中。取而代之的是,使用toString
似乎很有效。很难确定这实际上是api的正确用法。
这是另一种方式。这个想法是您获得的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());
}
String commonName = new LdapName(certificate.getSubjectX500Principal().getName()).getRdns().stream() .filter(i -> i.getType().equalsIgnoreCase("CN")).findFirst().get().getValue().toString();
CN
(aka 2.5.4.3
)Rdn#getValue()
包含一个String
。但是,对于自定义类型,结果为byte[]
(可能基于以开头的内部编码表示形式#
)。Ofc,byte[]
->String
是可能的,但包含其他(不可预测的)字符。我已使用基于BC的@laz解决方案解决了此问题,因为它在中正确处理和解码了此问题String
。
如果添加依赖关系不是问题,则可以使用Bouncy Castle的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在评论中提到的那样,这种方法现在已被弃用。请参阅gtrak使用新的Bouncy Castle API的更新代码。
作为不需要``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 :-(
X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();
使用:(使用Java 8)
IETFUtils.valueToString
返回的值逃脱形式。我发现只是调用.toString()
而是为我工作。
到目前为止发布的所有答案都有一个问题:大多数使用内部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(", "))
cert.getSubjectX500Principal().getName()
如果您不想依赖BouncyCastle,可以使用正则表达式over进行以下操作。
此正则表达式将解析一个专有名称,给予name
和val
每场比赛的捕获组。
当DN字符串包含逗号时,它们应加引号-此正则表达式可以正确处理带引号和不带引号的字符串,还可以处理带引号的字符串中的转义引号:
(?:^|,\s?)(?:(?<name>[A-Z]+)=(?<val>"(?:[^"]|"")+"|[^,]+))+
这是很好的格式:
(?:^|,\s?)
(?:
(?<name>[A-Z]+)=
(?<val>"(?:[^"]|"")+"|[^,]+)
)+
这是一个链接,因此您可以查看它的运行情况:https : //regex101.com/r/zfZX3f/2
如果您想让正则表达式仅获取CN,则此改编版本可以做到:
(?:^|,\s?)(?:CN=(?<val>"(?:[^"]|"")+"|[^,]+))
我有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();
更新:此类位于“ sun”包中,应谨慎使用。感谢Emil的评论:)
我只想分享,以获得CN,我这样做:
X500Name.asX500Name(cert.getSubjectX500Principal()).getCommonName();
关于Emil Lundberg的评论,请参阅:开发人员为什么不应该编写调用“ sun”包的程序
X500Name
是一种内部专有API,将来的发行版中可能会删除它。
确实,由于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;
可以使用cryptacular,这是在bouncycastle之上构建的Java密码库,易于使用。
RDNSequence dn = new NameReader(cert).readSubject();
return dn.getValue(StandardAttributeType.CommonName);
您可以尝试使用getName(X500Principal.RFC2253,oidMap)或getName(X500Principal.CANONICAL, oidMap)
查看哪种格式最适合DN字符串。也许一个oidMap
映射值将是你想要的字符串。
从证书中获取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();
使用纯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);
}
正则表达式非常昂贵。对于这样一个简单的任务,它可能会被淘汰。相反,您可以使用简单的String拆分:
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 "";
}
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);
}
BC使提取更加容易:
X500Principal principal = x509Certificate.getSubjectX500Principal();
X500Name x500name = new X500Name(principal.getName());
String cn = x500name.getCommonName();
sun.security.x509.X500Name
-几年前其他答案指出的这一点没有记载,因此不能依靠吗?
org.bouncycastle.asn1.x500.X500Name
类的JavaDoc ,但没有显示该方法……
对于多值属性-使用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;
}
}
}
}
}