今天,在单点登录系统中,使用中文用户名登录系统时,出现了返回的用户名乱码的问题。
通过阅读cas_client源码,找到了具体的原因。
获取用户名的操作是在ticket验证的过程中,下面,我先按照流程描述一下ticket验证的过程。
首先,由于我们在客户端进行了如下配置(代码1):
<filter>
<filter-name>CASValidationFilter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://localhost:8080/cas</param-value><!--cas服务器地址http://IP:PORT/CasWebProName-->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:8080</param-value><!--客户端服务器地址http://IP:PORT-->
</init-param>
</filter>
<filter-mapping>
<filter-name>CASValidationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
所以,在登陆成功以后,将进入Cas20ProxyReceivingTicketValidationFilter类。
AbstractTicketValidationFilter继承于AbstractCasFilter类。
AbstractCasFilter的doFilter方法如下(代码2):
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException
{
if (!preFilter(servletRequest, servletResponse, filterChain)) {
return;
}
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
String ticket = retrieveTicketFromRequest(request);
if (CommonUtils.isNotBlank(ticket))
{
this.logger.debug("Attempting to validate ticket: {}", ticket);
try
{
Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));
this.logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
request.setAttribute("_const_cas_assertion_", assertion);
if (this.useSession) {
request.getSession().setAttribute("_const_cas_assertion_", assertion);
}
onSuccessfulValidation(request, response, assertion);
if (this.redirectAfterValidation)
{
this.logger.debug("Redirecting after successful ticket validation.");
response.sendRedirect(constructServiceUrl(request, response));
return;
}
}
catch (TicketValidationException e)
{
this.logger.debug(e.getMessage(), e);
onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(e);
}
response.sendError(403, e.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
在该方法中使用ticketValidator对象调用validate方法进行ticket校验。
AbstractTicketValidationFilter继承了AbstractTicketValidationFilter的getTicketValidator方法并进行了实现(代码3):
protected final TicketValidator getTicketValidator(FilterConfig filterConfig)
{
boolean allowAnyProxy = getBoolean(ConfigurationKeys.ACCEPT_ANY_PROXY);
String allowedProxyChains = getString(ConfigurationKeys.ALLOWED_PROXY_CHAINS);
String casServerUrlPrefix = getString(ConfigurationKeys.CAS_SERVER_URL_PREFIX);
Class<? extends Cas20ServiceTicketValidator> ticketValidatorClass = getClass(ConfigurationKeys.TICKET_VALIDATOR_CLASS);
Cas20ServiceTicketValidator validator;
Cas20ServiceTicketValidator validator;
if ((allowAnyProxy) || (CommonUtils.isNotBlank(allowedProxyChains)))
{
Cas20ProxyTicketValidator v = (Cas20ProxyTicketValidator)createNewTicketValidator(ticketValidatorClass, casServerUrlPrefix, this.defaultProxyTicketValidatorClass);
v.setAcceptAnyProxy(allowAnyProxy);
v.setAllowedProxyChains(CommonUtils.createProxyList(allowedProxyChains));
validator = v;
}
else
{
validator = (Cas20ServiceTicketValidator)createNewTicketValidator(ticketValidatorClass, casServerUrlPrefix, this.defaultServiceTicketValidatorClass);
}
validator.setProxyCallbackUrl(getString(ConfigurationKeys.PROXY_CALLBACK_URL));
validator.setProxyGrantingTicketStorage(this.proxyGrantingTicketStorage);
HttpURLConnectionFactory factory = new HttpsURLConnectionFactory(getHostnameVerifier(), getSSLConfig());
validator.setURLConnectionFactory(factory);
validator.setProxyRetriever(new Cas20ProxyRetriever(casServerUrlPrefix, getString(ConfigurationKeys.ENCODING), factory));
validator.setRenew(getBoolean(ConfigurationKeys.RENEW));
validator.setEncoding(getString(ConfigurationKeys.ENCODING));
Map<String, String> additionalParameters = new HashMap();
List<String> params = Arrays.asList(RESERVED_INIT_PARAMS);
for (Enumeration<?> e = filterConfig.getInitParameterNames(); e.hasMoreElements();)
{
String s = (String)e.nextElement();
if (!params.contains(s)) {
additionalParameters.put(s, filterConfig.getInitParameter(s));
}
}
validator.setCustomParameters(additionalParameters);
return validator;
}
通过此方法可以获取代码2中需要使用的ticketValidator对象。
我们先看看ticketValidator对象都赋予了哪些值,在这里我只着重说一下(代码4):
validator.setProxyRetriever(new Cas20ProxyRetriever(casServerUrlPrefix, getString(ConfigurationKeys.ENCODING), factory));
这个参数在后边会讲到,但我们先看一下第二个参数的ConfigurationKeys.ENCODING的值(代码5):
public static final ConfigurationKey<String> ENCODING = new ConfigurationKey("encoding", null);
可以看到,第二个参数的值默认为null.
在上面提到了,在AbstractCasFilter的doFilter方法中使用ticketValidator对象调用validate进行ticket验证。
AbstractUrlBasedTicketValidator继承了TicketValidator并对TicketValidator方法进行了重写(代码6):
public final Assertion validate(String ticket, String service)
throws TicketValidationException
{
String validationUrl = constructValidationUrl(ticket, service);
this.logger.debug("Constructing validation url: {}", validationUrl);
try
{
this.logger.debug("Retrieving response from server.");
String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
}
this.logger.debug("Server response: {}", serverResponse);
return parseResponseFromServer(serverResponse);
}
catch (MalformedURLException e)
{
throw new TicketValidationException(e);
}
}
该方法返回了parseResponseFromServer(serverResponse);
parseResponseFromServer的代码如下(代码7)
protected final Assertion parseResponseFromServer(String response)
throws TicketValidationException
{
String error = XmlUtils.getTextForElement(response, "authenticationFailure");
if (CommonUtils.isNotBlank(error)) {
throw new TicketValidationException(error);
}
String principal = XmlUtils.getTextForElement(response, "user");
String proxyGrantingTicketIou = XmlUtils.getTextForElement(response, "proxyGrantingTicket");
String proxyGrantingTicket;
String proxyGrantingTicket;
if ((CommonUtils.isBlank(proxyGrantingTicketIou)) || (this.proxyGrantingTicketStorage == null)) {
proxyGrantingTicket = null;
} else {
proxyGrantingTicket = this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou);
}
if (CommonUtils.isEmpty(principal)) {
throw new TicketValidationException("No principal was found in the response from the CAS server.");
}
Map<String, Object> attributes = extractCustomAttributes(response);
Assertion assertion;
Assertion assertion;
if (CommonUtils.isNotBlank(proxyGrantingTicket))
{
AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes, proxyGrantingTicket, this.proxyRetriever);
assertion = new AssertionImpl(attributePrincipal);
}
else
{
assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
}
customParseResponse(response, assertion);
return assertion;
}
下面代码:
AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes, proxyGrantingTicket, this.proxyRetriever);
又涉及到了AttributePrincipalImpl类,在该类中有如下方法:
public String getProxyTicketFor(String service)
{
if (this.proxyGrantingTicket != null) {
return this.proxyRetriever.getProxyTicketIdFor(this.proxyGrantingTicket, service);
}
LOGGER.debug("No ProxyGrantingTicket was supplied, so no Proxy Ticket can be retrieved.");
return null;
}
又调用了下面的方法:
public String getProxyTicketIdFor(String proxyGrantingTicketId, String targetService)
{
CommonUtils.assertNotNull(proxyGrantingTicketId, "proxyGrantingTicketId cannot be null.");
CommonUtils.assertNotNull(targetService, "targetService cannot be null.");
URL url = constructUrl(proxyGrantingTicketId, targetService);
String response;
String response;
if (this.urlConnectionFactory != null) {
response = CommonUtils.getResponseFromServer(url, this.urlConnectionFactory, this.encoding);
} else {
response = CommonUtils.getResponseFromServer(url, this.encoding);
}
String error = XmlUtils.getTextForElement(response, "proxyFailure");
if (CommonUtils.isNotEmpty(error))
{
logger.debug(error);
return null;
}
return XmlUtils.getTextForElement(response, "proxyTicket");
}
下面重点来了,
public static String getResponseFromServer(URL constructedUrl, HttpURLConnectionFactory factory, String encoding)
{
HttpURLConnection conn = null;
InputStreamReader in = null;
try
{
conn = factory.buildHttpURLConnection(constructedUrl.openConnection());
if (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);
}
return builder.toString();
}
catch (Exception e)
{
LOGGER.error(e.getMessage(), e);
throw new RuntimeException(e);
}
finally
{
closeQuietly(in);
if (conn != null) {
conn.disconnect();
}
}
}
该方法使用HttpURLConnection向server端发起请求,获取到的返回结果为xml格式,并解析xml数据获取用户名。
可以看到:
if (isEmpty(encoding)) {
in = new InputStreamReader(conn.getInputStream());
} else {
in = new InputStreamReader(conn.getInputStream(), encoding);
}
当encoding为null,时,在定义输入流时不会指定编码格式,通过测试发现,此时读取中文自幅度会乱码。
所以我们需要在客户端的web.xml中按如下进行配置来指定编码格式:
<filter>
<filter-name>CASValidationFilter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://localhost:8080/cas</param-value><!--cas服务器地址http://IP:PORT/CasWebProName-->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:8080</param-value><!--客户端服务器地址http://IP:PORT-->
</init-param>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CASValidationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>