springboot security 集成 cas 问题 No subject alternative names present
- 前言
- 一、问题
- 1.实际问题
- 二、大海捞针
- 1.星星之火
- 2.通用方法
- 啰嗦一句
- 解决
- 2.新建三个类
- 配置文件修改
前言
场景:
在一次springboot security 集成 cas开发中,代码报错:java.security.cert.CertificateException: No subject alternative names present。
环境:
springboot security 集成 cas,cas 服务是其他厂家开发,使用https 协议,所以服务端无法改动,只能在客户端排查问题。
一、问题
从错误信息得到大概意思就是在调用https请求时候校验证书异常。
说到这个问题,我自己搭建一个 https服务, 使用 jdk 自带 keytool 工具生成证书验证了一下:
要能正确的调用https 请求, 要满足
1.客户端要导入由服务端通过 keystore 导出的证书文件;
2.服务生成证书时候填写的CN 信息(域名) 和客户端访问的url的域名要一致(本地测试可以修改hosts文件配置域名)。
1.实际问题
但是!
第三方厂家 cas 服务端的https 证书就随便搞的,连CN信息都是随便写的不知道啥意思。
而且,虽是https 但还是通过ip 端口方式访问。 *哎, 这明显是应付入网安评嘛 *
让我如何是好!
二、大海捞针
1.星星之火
网上搜了很多。
只能绕过 https 证书校验去访问了。
看到一篇和我同样问题的文章:记一次单点登录https中证书无效的问题
他的方式是修改 CommonUtils 源码,然后重新编译打包替换原来的包,这种方式虽说能解决但是不通用,为啥尼,比如代码需要重新下载依赖,那还是每次都要重新替换,而且其他项目想校验证书用到了这个类,就难办了。
而且。他的答案漏了一行重要的代码,下文会提到。
2.通用方法
对于java 需要修改源码,我们一般会使用继承重写的方式。
但是对应本项目。
问题就出现在 CommonUtils类中 getResponseFromServer 这个方法。不能在这地方直接修改,否则只能像刚才那个网友搞了。找找到哪里用到这个地方。
往前找
AbstractCasProtocolUrlBasedTicketValidator.class-retrieveResponseFromServer 一看 final 修饰,没戏,再往前找。
AbstractUrlBasedTicketValidator.class - validate 该方法被final 修饰,重写不了。再往前
CasAuthenticationProvider.class - authenticateNow , 这个是 private 私有方法,重写不了。
再往前
CasAuthenticationProvider.class - authenticate ,哎CasAuthenticationProvider,好熟悉的眼神, 没错就是你了,在配置cas就见过你。再加上这个方法是 public,于是就重写这个方法,从这开刀。
啰嗦一句
建议新手多多学会使用 debuger, 你会发现越来越上瘾
解决
2.新建三个类
CasAuthenticationProviderExt
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package cn.com.liyx.learn.lyxsec.ext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.*;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.*;
import org.springframework.util.Assert;
public class CasAuthenticationProviderExt extends CasAuthenticationProvider {
private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class);
private final UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
private Cas20ProxyTicketValidatorExt ticketValidator;
private ServiceProperties serviceProperties;
public void afterPropertiesSet() {
Assert.notNull(this.ticketValidator, "A ticketValidator must be set");
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!this.supports(authentication.getClass())) {
return null;
} else if (authentication instanceof UsernamePasswordAuthenticationToken && !"_cas_stateful_".equals(authentication.getPrincipal().toString()) && !"_cas_stateless_".equals(authentication.getPrincipal().toString())) {
return null;
} else if (authentication instanceof CasAuthenticationToken) {
if (getKey().hashCode() == ((CasAuthenticationToken) authentication).getKeyHash()) {
return authentication;
} else {
throw new BadCredentialsException(this.messages.getMessage("CasAuthenticationProviderExt.incorrectKey", "The presented CasAuthenticationToken does not contain the expected key"));
}
} else if (authentication.getCredentials() != null && !"".equals(authentication.getCredentials())) {
boolean stateless = false;
if (authentication instanceof UsernamePasswordAuthenticationToken && "_cas_stateless_".equals(authentication.getPrincipal())) {
stateless = true;
}
CasAuthenticationToken result = null;
if (stateless) {
result = getStatelessTicketCache().getByTicketId(authentication.getCredentials().toString());
}
if (result == null) {
result = this.authenticateNow(authentication);
result.setDetails(authentication.getDetails());
}
if (stateless) {
getStatelessTicketCache().putTicketInCache(result);
}
return result;
} else {
throw new BadCredentialsException(this.messages.getMessage("CasAuthenticationProviderExt.noServiceTicket", "Failed to provide a CAS service ticket to validate"));
}
}
private CasAuthenticationToken authenticateNow(Authentication authentication) throws AuthenticationException {
try {
Assertion assertion = this.ticketValidator.validate2(authentication.getCredentials().toString(), this.getServiceUrl(authentication));
UserDetails userDetails = this.loadUserByAssertion(assertion);
userDetailsChecker.check(userDetails);
return new CasAuthenticationToken(getKey(), userDetails, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()), userDetails, assertion);
} catch (TicketValidationException var4) {
throw new BadCredentialsException(var4.getMessage(), var4);
}
}
private String getServiceUrl(Authentication authentication) {
String serviceUrl;
if (authentication.getDetails() instanceof ServiceAuthenticationDetails) {
serviceUrl = ((ServiceAuthenticationDetails) authentication.getDetails()).getServiceUrl();
} else {
if (this.serviceProperties == null) {
throw new IllegalStateException("serviceProperties cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");
}
if (this.serviceProperties.getService() == null) {
throw new IllegalStateException("serviceProperties.getService() cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");
}
serviceUrl = this.serviceProperties.getService();
}
if (logger.isDebugEnabled()) {
logger.debug("serviceUrl = " + serviceUrl);
}
return serviceUrl;
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
public void setServiceProperties(ServiceProperties serviceProperties) {
this.serviceProperties = serviceProperties;
}
protected Cas20ProxyTicketValidatorExt getTicketValidator() {
return this.ticketValidator;
}
public void setTicketValidator(Cas20ProxyTicketValidatorExt ticketValidator) {
this.ticketValidator = ticketValidator;
}
}
Cas20ProxyTicketValidatorExt
package cn.com.liyx.learn.lyxsec.ext;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.Cas20ProxyTicketValidator;
import org.jasig.cas.client.validation.TicketValidationException;
import java.net.MalformedURLException;
import java.net.URL;
public class Cas20ProxyTicketValidatorExt extends Cas20ProxyTicketValidator {
public Cas20ProxyTicketValidatorExt(String casServerUrlPrefix) {
super(casServerUrlPrefix);
}
public final Assertion validate2(String ticket, String service) throws TicketValidationException {
String validationUrl = this.constructValidationUrl(ticket, service);
this.logger.debug("Constructing validation url: {}", validationUrl);
try {
this.logger.debug("Retrieving response from server.");
// String serverResponse = this.retrieveResponseFromServer(new URL(validationUrl), ticket);
String serverResponse = CommonUtilsExt.getResponseFromServer(new URL(validationUrl), this.getURLConnectionFactory(), this.getEncoding());
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
} else {
this.logger.debug("Server response: {}", serverResponse);
return this.parseResponseFromServer(serverResponse);
}
} catch (MalformedURLException var5) {
throw new TicketValidationException(var5);
}
}
}
CommonUtilsExt
package cn.com.liyx.learn.lyxsec.ext;
import org.jasig.cas.client.ssl.HttpURLConnectionFactory;
import org.jasig.cas.client.util.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
public class CommonUtilsExt {
private static final Logger LOGGER = LoggerFactory.getLogger(CommonUtils.class);
static {
disableSslVerification();
}
private static void disableSslVerification() {
try {
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}
};
// Install the all-trusting trust manager
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
// Create all-trusting host name verifier
HostnameVerifier allHostsValid = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
// Install the all-trusting host verifier
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
}
public static String getResponseFromServer(URL constructedUrl, HttpURLConnectionFactory factory, String encoding) {
HttpURLConnection conn = null;
InputStreamReader in = null;
try {
// conn = factory.buildHttpURLConnection(constructedUrl.openConnection()); // 原来的写法。
conn = (HttpURLConnection) constructedUrl.openConnection(); // 新的写法, (注)本文要这样的写法
if (CommonUtils.isEmpty(encoding)) {
in = new InputStreamReader(conn.getInputStream());
} else {
in = new InputStreamReader(conn.getInputStream(), encoding);
}
StringBuilder builder = new StringBuilder(255);
int byteRead;
while ((byteRead = in.read()) != -1) {
builder.append((char) byteRead);
}
String var7 = builder.toString();
return var7;
} catch (RuntimeException var13) {
throw var13;
} catch (SSLException var14) {
LOGGER.error("SSL error getting response from host: {} : Error Message: {}", new Object[]{constructedUrl.getHost(), var14.getMessage(), var14});
throw new RuntimeException(var14);
} catch (IOException var15) {
LOGGER.error("Error getting response from host: [{}] with path: [{}] and protocol: [{}] Error Message: {}", new Object[]{constructedUrl.getHost(), constructedUrl.getPath(), constructedUrl.getProtocol(), var15.getMessage(), var15});
throw new RuntimeException(var15);
} finally {
CommonUtils.closeQuietly(in);
if (conn != null) {
conn.disconnect();
}
}
}
}
前面说那个网友漏掉重要一行, 就是下图这一行, 确实重要!!!
配置文件修改
注意就是方法返回类型 以及 属性类型也需要用新增的类名,不然会莫名报错。
完工。