javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative DNS name matching cas-qa.linesum.com found.
at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1747)
at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:241)
at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:235)
at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1209)
at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:135)
at com.sun.net.ssl.internal.ssl.Handshaker.processLoop(Handshaker.java:593)
at com.sun.net.ssl.internal.ssl.Handshaker.process_record(Handshaker.java:529)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:943)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1188)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1215)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1199)
at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:434)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:166)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1195)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:234)
at org.jasig.cas.client.util.CommonUtils.getResponseFromServer(CommonUtils.java:326)
at org.jasig.cas.client.util.CommonUtils.getResponseFromServer(CommonUtils.java:305)
at org.jasig.cas.client.validation.AbstractCasProtocolUrlBasedTicketValidator.retrieveResponseFromServer(AbstractCasProtocolUrlBasedTicketValidator.java:50)
at org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator.validate(AbstractUrlBasedTicketValidator.java:207)
at org.apache.shiro.cas.CasRealm.doGetAuthenticationInfo(CasRealm.java:144)
at cn.rxxxx.xxxx.server.shiro.realm.UapDbRealm.doGetAuthenticationInfo(UapDbRealm.java:80)
at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:568)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:267)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:270)
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256)
at org.apache.shiro.web.filter.authc.AuthenticatingFilter.executeLogin(AuthenticatingFilter.java:53)
at org.apache.shiro.cas.CasFilter.onAccessDenied(CasFilter.java:85)
at org.apache.shiro.web.filter.AccessControlFilter.onAccessDenied(AccessControlFilter.java:133)
at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162)
at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:203)
at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:178)
at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:131)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
什么是 Subject Alternative name?
异常中no subject alternative DNS name让我很好奇,这是一个什么字段,于是查了一下证书的一些知识。
The Subject Alternative Name field lets you specify additional host names (sites, IP addresses, common names, etc.) to be protected by a single SSL Certificate, such as a Multi-Domain (SAN) or Extend Validation Multi-Domain Certificate.
后来证书机构调整了SAN中域名的顺序,放到了测试环境进行测试,果然就可以使用了。然而在正式环境中去使用,仍然发生错误,还是之前No subject alternative DNS name matching的异常。于是我将测试环境的nginx配置修改为和正式环境nginx配置格式一致方式进行测试,此时测试环境也出现同样问题。我又在本机通过如下代码访问https://cas-qa/linesum.com进行测试发现了某种异常:
With this configuration a browser receives the default server’s certificate, i.e. www.example.com regardless of the requested server name. This is caused by SSL protocol behaviour. The SSL connection is established before the browser sends an HTTP request and nginx does not know the name of the requested server. Therefore, it may only offer the default server’s certificate.
A more generic solution for running several HTTPS servers on a single IP address is TLS Server Name Indication extension (SNI, RFC 6066), which allows a browser to pass a requested server name during the SSL handshake and, therefore, the server will know which certificate it should use for the connection. However, SNI has limited browser support. Currently it is supported starting with the following browsers versions:
Opera 8.0;
MSIE 7.0 (but only on Windows Vista or higher);
Firefox 2.0 and other browsers using Mozilla Platform rv:1.8.1;
Safari 3.2.1 (Windows version supports SNI on Vista or higher);
and Chrome (Windows version supports SNI on Vista or higher, too).
In order to use SNI in nginx, it must be supported in both the OpenSSL library with which the nginx binary has been built as well as the library to which it is being dynamically linked at run time. OpenSSL supports SNI since 0.9.8f version if it was built with config option “--enable-tlsext”. Since OpenSSL 0.9.8j this option is enabled by default. If nginx was built with SNI support, then nginx will show this when run with the “-V” switch:
惊现问题
最近在做域名切换的时候,从某个业务系统登录进行统一身份服务时候出现了如下异常:在此之前,我们使用的是xx.cn 目前改为xx.com
什么是 Subject Alternative name?
异常中
no subject alternative DNS name
让我很好奇,这是一个什么字段,于是查了一下证书的一些知识。可以看出subject alternative name 可以定义多个域名、ip地址等,以实现一个证书认证多个地址、多个域名。具体的解释可以参考Multi-Domain (SAN) Certificates - Using Subject Alternative Names 具体可以参见如下图片:
所以之前那个问题应该是证书中DNS name 字段值无法匹配cas.linesum.com.
于是打开证书去做比较,先记录几个在线查看的工具:
差别参见
左边是com证书 右边是cn证书,cn证书目前运行正常而com运行存在问题,于是做了对比发现com证书也仅是与cn证书顺序不一样而已。
初识SNI(服务器名称指示)
后来证书机构调整了SAN中域名的顺序,放到了测试环境进行测试,果然就可以使用了。然而在正式环境中去使用,仍然发生错误,还是之前
No subject alternative DNS name matching
的异常。于是我将测试环境的nginx配置修改为和正式环境nginx配置格式一致方式进行测试,此时测试环境也出现同样问题。我又在本机通过如下代码访问https://cas-qa/linesum.com
进行测试发现了某种异常:那么问题来了,为什么我请求的是com的域名,服务端却给我一个cn的证书?那为什么用浏览器去访问com域名却没有证书不安全的提示呢? nginx 关于如何配置https服务器(Configuring HTTPS servers)也给予了一些提示:
这一信息指出执行SSL协议是在http通信之前,此时nginx并不知道客户端请求的是哪一个服务器。所以我在nginx上配置了com和cn两个域名的https服务且cn的服务在前,使得其成为了默认https服务。当我的程序请求的时候,nginx不知道我要访问哪个https服务,于是就给了一个默认服务即cn证书,导致了上述异常的发生。后来去调整Nginx com和cn的顺序果然解决了异常。 这就解答了
为什么我请求的是com的域名,服务端却给我一个cn的证书
这个问题,然而那为什么用浏览器去访问com域名却没有证书不安全的提示呢?
同样是在nginx这边说明文章中提到:也就是说浏览器存在支持TLS 的SNI协议扩展,允许在SSL执行握手的时候向服务器发送请求的服务名信息,使得服务端可以判断需要给客户端什么证书,实现一个IP使用多个域名和多个证书的需求。于是这里又引出一个概念叫
SNI(服务器名称指示)
具体可以参考 SNI 服务器名称指示。 但其中最核心的一句话:解答了上述两个疑惑之后,如何解决java代码请求拥有多个证书的https服务器的异常呢?首先要检测一下我们的nginx是否支持SNI。
得知目前服务器nginx是支持SNI的,那就需要检验一下我们的客户端是否支持该协议。
Java 对SNI的支持
经过查询发现java 6从
6u121 b31
版本开始就支持SNI协议,具体可以参见Java SE 6 Advanced and Java SE 6 Support 。java 8 存在的SNI的BUG
本人在java 8中设置
CertificateHostNameVerifier()
执行之前的测试代码依然有这样的问题,Java SSL handshake with Server Name Identification (SNI)这篇文章给出了解决方案,文中提到java8中存在一旦设置了HostNameVerifier就会丢失SNI信息的bug,具体可以参见Custom HostnameVerifier disables SNI extension 该文提到了一个解决方案,经过实测确实有效。但这个bug需要在java 9中才能得以解决。总的而言对于java 8来说不需要设置自定义的HostNameVerifier,就可以规避这个问题。java 中如何调试SSL协议
增加java vm启动参数
-Djavax.net.debug=ssl:handshake
或者执行System.setProperty("javax.net.debug","ssl:handshake")
。这样涉及到https请求的时候,所有SSL之间握手过程都会以日志形式在控制台中呈现。错误是如何发生的?
掌握了调试SSL协议的方法之后,我们就可以重新事故发生现场,仍然使用如下测试代码:
在java 6中SSL 握手过程如下所示
从java 6 早期版本 SSL握手过程可以看到,服务端最终发来的是cn证书,这也就是导致后续认证域名与请求域名匹配异常的原因所在,那我们再看看java 8中的SSL握手。如下所示:
在java 8中在client hello阶段会传递如下数据
Extension server_name, server_name: [type=host_name (0), value=cas-qa.linesum.com]
这就是前面提及的SNI信息。我们会看到此时服务端能够正确返回com证书。最后的解决方案
知道了那么多道理之后,生活依然过的不好。由于当前各个业务系统服务器所使用的都是早期jdk 6版本,而且我们的CAS client包中在执行https默认会使用
DefaultHostNameVerifier
(之前提及的java 8 Bug也会出现),所以即使升级到java 8 也没办法很好解决。最后的办法是将cas client包中设置使用AnyHostNameVerifier
即客户端不检验服务端。