單點登入(十六)-----遇到問題-----cas4.2.x登入成功後報錯No principal was found---cas中文亂碼問題完美解決
情況
我們之前已經完成了cas4.2.x登入使用mongodb驗證方式並且自定義了加密。
但是悲劇的是 當用戶名是中文名時或者獲取的其他屬性中有中文名時成功登入後報錯No principal was found。
javax.servlet.ServletException: org.jasig.cas.client.validation.TicketValidationException: No principal was found in the response from the CAS server.
org.jasig.cas.client.validation.AbstractTicketValidationFilter.doFilter(AbstractTicketValidationFilter.java:155)
org.jasig.cas.client.authentication.AuthenticationFilter.doFilter(AuthenticationFilter.java:102)
org.jasig.cas.client.session.SingleSignOutFilter.doFilter(SingleSignOutFilter.java:110)
root cause
org.jasig.cas.client.validation.TicketValidationException: No principal was found in the response from the CAS server.
org.jasig.cas.client.validation.Cas20ServiceTicketValidator.parseResponseFromServer(Cas20ServiceTicketValidator.java:82)
org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator.validate(AbstractUrlBasedTicketValidator.java:188)
org.jasig.cas.client.validation.AbstractTicketValidationFilter.doFilter(AbstractTicketValidationFilter.java:132)
org.jasig.cas.client.authentication.AuthenticationFilter.doFilter(AuthenticationFilter.java:102)
org.jasig.cas.client.session.SingleSignOutFilter.doFilter(SingleSignOutFilter.java:110)
原因
我這裡使用的是cas server 4.2.7 ,cas client的版本通過maven引入3.4.1。
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.4.1</version>
</dependency>
我們在console中檢視報錯原因有
ervlet.service() for servlet jsp threw exception
org.xml.sax.SAXParseException: The element type "cas:user" must be terminated by the matching end-tag "</cas:user>"
說明user的返回值中,中文變成了亂碼導致異常丟擲。
我們來分析下登入的流程。
登入認證成功後會認證ticket然後跳轉到cas client。
認證ticket和承載轉發到cas client的資訊是由casServiceValidationSuccess.jsp頁面來完成的。但是cas-server-webapp中是有2.0和3.0版本的協議的。
我們在cas client中定義了使用cas的哪種協議。
cas2.0協議的驗證結果頁面是通過目錄:cas/WEB-INF/view/jsp/protocol/2.0/ 下定義的jsp(jstlview)模板頁面來定義的。
casServiceValidationSuccess
casServiceValidationFailure.jsp:對應驗證失敗頁面。
所以問題就出在casServiceValidationSuccess.jsp頁面的編碼以及cas client這邊接受這個頁面編碼的問題上。
cas client這邊接受這個頁面編碼是在cas-client-core-3.4.1的專案裡Cas20ProxyReceivingTicketValidationFilter.java中完成的。
只要casServiceValidationSuccess.jsp頁面的編碼和Cas20ProxyReceivingTicketValidationFilter.java使用的編碼一致就不會出現接受資訊的亂碼問題。
我們可以看到cas server 4.2.7的jsp頁面是使用 utf-8編碼的。
在頁面的頭部有:
<%@ page session="false" contentType="application/xml; charset=utf-8" %>
但是cas-client-core-3.4.1的專案裡Cas20ProxyReceivingTicketValidationFilter.java中,getTicketValidator方法裡設定的預設編碼方式是null。
根據測試當預設編碼方式是null時只能識別到GBK模式或者gb2312的編碼。
解決方案
知道了方案之後我們就可以開始解決了。之前查了很多網上的說法,都只說到了一些某一種方式,而且不是全都適用。
這裡我們知道了 亂碼的原因之後 就可以從根本上解決亂碼問題。
也就是需要讓 casServiceValidationSuccess.jsp頁面的編碼和Cas20ProxyReceivingTicketValidationFilter.java使用的編碼一致就不會出現接受資訊的亂碼問題。
解決方案一casServiceValidationSuccess.jsp使用gbk編碼
最簡單的一種修改方法,將 casServiceValidationSuccess.jsp頁面的編碼修改為gbk或者gb2312。
因為我們之前已經說過 Cas20ProxyReceivingTicketValidationFilter.java的編碼模式預設是null時可以識別到gbk和gb2312的編碼的。
所以我們只需要把 casServiceValidationSuccess.jsp頁面的
<%@ page session="false" contentType="application/xml; charset=utf-8" %>
修改成
<%@ page session="false" contentType="application/xml; charset=gbk" %>
或者
<%@ page session="false" contentType="application/xml; charset=gb2312" %>
即可。
解決方案二修改原始碼兩個檔案都使用utf-8編碼
有些同學會說了,我們的cas server專案和cas client專案都使用的utf-8,為什麼casServiceValidationSuccess.jsp要使用gbk編碼,統一改成utf-8行不行。
答案是可以的。
那我們就需要同時修改兩個檔案了。
首先casServiceValidationSuccess.jsp使用utf-8編碼。也就是頭部保持:
<%@ page session="false" contentType="application/xml; charset=utf-8" %>
然後
如果我們的cas-client-core-3.4.1是子專案的形式那麼我們就可以直接修改Cas20ProxyReceivingTicketValidationFilter.java檔案中的getTicketValidator方法裡設定的預設編碼方式是utf-8。
validator.setProxyRetriever(new Cas20ProxyRetriever(casServerUrlPrefix,"utf-8", factory));
validator.setEncoding("utf-8");
解決方案三配置法兩個檔案都使用utf-8編碼
有些同學會說 我們的cas-client-core-3.4.1用的不是專案引入形式,而是一個jar包,不能直接修改原始碼怎麼辦,其實可以在配置檔案上修改。(這種方式不生效請看方案四,有講原因)
我們在之前的ConfigurationKeys中就看到encoding是引數配置的形式可以引入的。
那麼我們還是修改兩個檔案一個是casServiceValidationSuccess.jsp另一個是cas client的web.xml中配置
首先casServiceValidationSuccess.jsp使用utf-8編碼。也就是頭部保持:
<%@ page session="false" contentType="application/xml; charset=utf-8" %>
然後
web.xml中找到
<!-- 該過濾器負責對Ticket的校驗工作,必須-->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://127.0.0.1:8080/cas/</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://127.0.0.1:8080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<!-- 對專案中的哪些路徑做登入攔截-->
<url-pattern>/*</url-pattern>
</filter-mapping>
需要加入encoding引數配置
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
修改後的配置為:
<!-- 該過濾器負責對Ticket的校驗工作,必須-->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://127.0.0.1:8080/cas/</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://127.0.0.1:8080</param-value>
</init-param>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<!-- 對專案中的哪些路徑做登入攔截-->
<url-pattern>/*</url-pattern>
</filter-mapping>
解決方案四使用URLEncoder來暴力相互轉碼
有些同學說怎麼用配置法設定之後不生效,還是亂碼? 我也遇到了。跟進原始碼裡發現 這一個cas client中使用的cas-client-core的版本是3.1.10。
cas-client-core-3.1.10裡竟然沒有用到配置引數encoding。。。。
所以配置法在低版本的cas-client-core中是無效的。
這種情況的話 只能下載cas-client-core3.1.10原始碼直接修改程式碼。
如果是jar包引用的話 還有另一種比較變態的改法,就是使用URLEncoder來暴力相互轉碼。
首先casServiceValidationSuccess.jsp使用utf-8編碼。也就是頭部修改為(多加import="java.net.URLEncoder"):
<%@ page session="false" contentType="application/xml; charset=utf-8" import="java.net.URLEncoder" %>
然後
casServiceValidationSuccess.jsp頁面裡的 <cas:user>${fn:escapeXml(principal.id)}</cas:user>
修改成
<c:set var="val" value="${fn:escapeXml(principal.id)}"/>
<cas:user><%=URLEncoder.encode((String)pageContext.getAttribute("val"),"UTF-8")%></cas:user>
即可。
這裡處理的只是登入名屬性。
如果是其他屬性的話 需要新增
<cas:attributes>
<c:forEach items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"var="attr">
<c:set var="val" value="${attr.value}"/>
<cas:${attr.key}><%=URLEncoder.encode((String)pageContext.getAttribute("val"),"UTF-8")%></cas:${attr.key}>
</c:forEach>
</cas:attributes>
則獲取多屬性的casServiceValidationSuccess.jsp最終配置為
<%@ page session="false" contentType="application/xml; charset=utf-8" import="java.net.URLEncoder" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<c:set var="val" value="${fn:escapeXml(principal.id)}"/>
<cas:user><%=URLEncoder.encode((String)pageContext.getAttribute("val"),"UTF-8")%></cas:user>
<c:if test="${not empty pgtIou}">
<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
</c:if>
<c:if test="${fn:length(chainedAuthentications) > 0}">
<cas:proxies>
<c:forEach var="proxy" items="${chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(chainedAuthentications)}" step="1">
<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
</c:forEach>
</cas:proxies>
</c:if>
<cas:attributes>
<c:forEach items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"var="attr">
<c:set var="val" value="${attr.value}"/>
<cas:${attr.key}><%=URLEncoder.encode((String)pageContext.getAttribute("val"),"UTF-8")%></cas:${attr.key}>
</c:forEach>
</cas:attributes>
</cas:authenticationSuccess>
</cas:serviceResponse>
只獲取id的casServiceValidationSuccess.jsp最終配置為
<%@ page session="false" contentType="application/xml; charset=utf-8" import="java.net.URLEncoder" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<c:set var="val" value="${fn:escapeXml(principal.id)}"/>
<cas:user><%=URLEncoder.encode((String)pageContext.getAttribute("val"),"UTF-8")%></cas:user>
<c:if test="${not empty pgtIou}">
<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
</c:if>
<c:if test="${fn:length(chainedAuthentications) > 0}">
<cas:proxies>
<c:forEach var="proxy" items="${chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(chainedAuthentications)}" step="1">
<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
</c:forEach>
</cas:proxies>
</c:if>
</cas:authenticationSuccess>
</cas:serviceResponse>
而且如果是轉碼設定則客戶端取值的時候也需要轉碼,所以說有點變態,還是建議前三種解決方案。
cas client在jsp中獲取
<%
Assertion assertion = AssertionHolder.getAssertion();
AttributePrincipal ap = assertion.getPrincipal();
String id = ap.getName();
Map<String,Object> att = ap.getAttributes();
out.print("<br/>"+id);
out.print("<br/>"+att);
String name = URLDecoder.decode(""+att.get("username"), "UTF-8");
out.println("<br/>"+name);
%>
cas client在java中獲取
HttpServletRequest request = ServletActionContext.getRequest();
/*獲取單點登入伺服器傳遞過來的使用者資訊*/
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
if (principal != null) {
Map<String, Object> attributes = principal.getAttributes();
if (attributes.size() > 0) {
URLDecoder.decode(""+attributes.get("userid"), "UTF-8");
URLDecoder.decode(""+attributes.get("username"), "UTF-8");
}
}
啟示
在使用cas過程中有些配置不起作用需要跟蹤到原始碼裡看看,有可能是版本不同的問題。