开源的 CAS 单点登录本身已经提供了一个统一的登录页面,也就是我们配置好之后,所有的没有登录的请求都会拦截到自带的登录页面,我们可以根据自己的需求来改造这个页面,但是,需求是多种多样的并且有时候总是更改的,我就碰到过这种情况,有时就会让人很崩溃。
很多时候,我们都是接手别人的代码,而且大多都是比较成熟,并且有一定时间的项目,我们总是在前人的基础进行修修补补。就拿单点登录来说,我相信大多数项目都有一套自己比较成熟的登录过滤机制,如果我们集成单点登录功能,也是使用我们以前的登录页面,这时候我们就要对 CAS 进行改造。
在我第一次接手这个需求之前,我在网上查找了很多资料,大多数都是教我们将 CAS 集成进项目,我在集成的过程发现,因为我的项目本来就是一个老项目,使用的是 Spring Security 来进行登录过滤,要将原来的登录逻辑改成 CAS 的,这个比较难,往往会碰到很多 Bug,代码的耦合度有点高,牵一发而动全身,不胜其烦。所以,我感觉这个方法并不适合一个老项目。后来,我想了一个办法,另外单独写了一个登录的服务,所有的请求都拦截到这个新的服务,然后根据访问的链接,验证成功后再跳转回去,这样做的好处有,可扩展性比较强,以后如果有新的项目要加进来,只需添加一些配置即可,缺点就是需要单独启动一个服务,这样开销较大,要是这个服务崩,其他的也登录不进去了。
这时,我们就要发挥搜索引擎的巨大优势和身为程序员的主观能动性了,在我一通搜索之后,我终于找到了一个比较合适的解决方案了,那就是改造 CAS,自定义登录页面。
方法
将 CAS 原有的过滤器改为我们自己的过滤器,新增一个 remoteLogin 类,将 CAS 原有的处理登录的类换成我们自己的,其他的校验还是 CAS 自己的,新增一个校验登录失败的页面,将错误信息返回给客户端。我们需要修改客户端和 CAS 认证服务器端两处。
修改客户端
1. 新增 RemoteAuthenticationFilter.java,可以自定义路径,web.xml 文件能够正确引用即可。
public class RemoteAuthenticationFilter extends AbstractCasFilter {
public static final String CONST_CAS_GATEWAY = "_const_cas_gateway_";
/**.
* 本地登陆页面URL.
*/
private String localLoginUrl;
/**.
* The URL to the CAS Server login.
*/
private String casServerLoginUrl;
/**.
* Whether to send the renew request or not.
*/
private boolean renew = false;
/**.
* Whether to send the gateway request or not.
*/
private boolean gateway = false;
protected void initInternal(final FilterConfig filterConfig) throws ServletException {
super.initInternal(filterConfig);
setCasServerLoginUrl(getPropertyFromInitParams(filterConfig, "casServerLoginUrl", null));
log.trace("Loaded CasServerLoginUrl parameter: " + this.casServerLoginUrl);
setLocalLoginUrl(getPropertyFromInitParams(filterConfig, "localLoginUrl", null));
log.trace("Loaded LocalLoginUrl parameter: " + this.localLoginUrl);
setRenew(Boolean.parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false")));
log.trace("Loaded renew parameter: " + this.renew);
setGateway(Boolean.parseBoolean(getPropertyFromInitParams(filterConfig, "gateway", "false")));
log.trace("Loaded gateway parameter: " + this.gateway);
}
public void init() {
super.init();
CommonUtils.assertNotNull(this.localLoginUrl, "localLoginUrl cannot be null.");
CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final HttpSession session = request.getSession(false);
final String ticket = request.getParameter(getArtifactParameterName());
final Assertion assertion = session != null
? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
final boolean wasGatewayed = session != null && session.getAttribute(CONST_CAS_GATEWAY) != null;
// 如果访问路径为localLoginUrl且带有validated参数则跳过
URL url = new URL(localLoginUrl);
final boolean isValidatedLocalLoginUrl = request.getRequestURI().endsWith(url.getPath())
&& CommonUtils.isNotBlank(request.getParameter("validated"));
if (!isValidatedLocalLoginUrl && CommonUtils.isBlank(ticket) && assertion == null
&& !wasGatewayed) {
log.debug("no ticket and no assertion found");
if (this.gateway) {
log.debug("setting gateway attribute in session");
request.getSession(true).setAttribute(CONST_CAS_GATEWAY, "yes");
}
final String serviceUrl = constructServiceUrl(request, response);
if (log.isDebugEnabled()) {
log.debug("Constructed service url: " + serviceUrl);
}
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
getServiceParameterName(), serviceUrl, this.renew, this.gateway);
// 加入localLoginUrl
urlToRedirectTo += (urlToRedirectTo.contains("?") ? "&" : "?") + "loginUrl="
+ URLEncoder.encode(localLoginUrl, "utf-8");
if (log.isDebugEnabled()) {
log.debug("redirecting to '" + urlToRedirectTo + "'");
}
response.sendRedirect(urlToRedirectTo);
return;
}
if (session != null) {
log.debug("removing gateway attribute from session");
session.setAttribute(CONST_CAS_GATEWAY, null);
}
filterChain.doFilter(request, response);
}
public final void setRenew(final boolean renew) {
this.renew = renew;
}
public final void setGateway(final boolean gateway) {
this.gateway = gateway;
}
public final void setCasServerLoginUrl(final String casServerLoginUrl) {
this.casServerLoginUrl = casServerLoginUrl;
}
public final void setLocalLoginUrl(String localLoginUrl) {
this.localLoginUrl = localLoginUrl;
}
}
2. web.xml ,将原来的过滤器换成上面的过滤器,然后配置以下配置。如果要实现单点登出功能,只需将单点登出的配置放到最上面,使用 CAS 自带的单点登出功能即可。
<!-- 单点登出 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<!-- 只需改成 CAS 认证服务器的地址,例如 -->
<param-value>http://127.0.0.1:8080/cas</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 单点登录 Start -->
<filter>
<filter-name>CAS Filter</filter-name>
<!-- 这里换成自己的包 -->
<filter-class>com.demo.filter.RemoteAuthenticationFilter</filter-class>
<init-param>
<param-name>localLoginUrl</param-name>
<!-- 这里放我们自己项目的登陆页地址,例如 -->
<param-value>http://localhost:8082/myProject/login.do</param-value>
</init-param>
<init-param>
<param-name>casServerLoginUrl</param-name>
<!-- 只需改 CAS 认证服务器的地址 -->
<param-value>http://127.0.0.1:8080/cas/remoteLogin</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!-- 只需改项目的 IP 和端口 -->
<param-value>http://localhost:8082/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- CAS Client向CAS Server进行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>
<!-- 只需改 CAS 认证服务器的 IP 和端口 -->
<param-value>http://127.0.0.1:8080/cas/</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!-- 只需改项目的 IP 和端口 -->
<param-value>http://localhost:8082/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 封装request, 支持getUserPrincipal等方法-->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 存放Assertion到ThreadLocal中 -->
<filter>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 单点登录 End -->
3. login.jsp,在登录页面加上以下代码
function getParam(name) {
var queryString = window.location.search;
var param = queryString.substr(1, queryString.length - 1).split("&");
for (var i = 0; i < param.length; i++) {
var keyValue = param[i].split("=");
if (keyValue[0] == name) {
return keyValue[1];
}
}
return null;
}
function getParamVal(name, queryString) {
var param = queryString.substr(1, queryString.length - 1).split("&");
for (var i = 0; i < param.length; i++) {
var keyValue = param[i].split("=");
if (keyValue[0] == name) {
return keyValue[1];
}
}
return null;
}
function init() {
// 显示异常信息
var error = getParam("errorMessage");
if (error) {
var errorStr = decodeURIComponent(error);
$("#errorMessage").html(errorStr);
}
// 注入service
var service = getParam("service");
if (service) {
var serviceStr = decodeURIComponent(service);
$("#service").val(serviceStr);
var errorMsg = getParamVal("errorMessage", service);
if (errorMsg != null) {
$("#errorMessage").html(decodeURIComponent(errorMsg));
}
} else {
$("#service").val(location.href);
}
}
<form id="form1" name="form1" method="post" onsubmit="init()" action="http:/localhost:8080/cas/remoteLogin">
<input type="hidden" name="loginUrl" value="http://localhost:8082/demo/login.do">
<input type="hidden" name="submit" value="true" />
<input type="hidden" name="service" id= "service"/> />
修改 CAS 服务端
1. 修改 web.xml 文件,添加 remoteLogin 的映射
<servlet-mapping>
<servlet-name>cas</servlet-name>
<url-pattern>/remoteLogin</url-pattern>
</servlet-mapping>
- 增加 RemoteLoginAction 登录处理类
public class RemoteLoginAction extends AbstractAction{
/** CookieGenerator for the Warnings. */
@NotNull
private CookieRetrievingCookieGenerator warnCookieGenerator;
/** CookieGenerator for the TicketGrantingTickets. */
@NotNull
private CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator;
/** Extractors for finding the service. */
@NotNull
@Size(min = 1)
private List<ArgumentExtractor> argumentExtractors;
/** Boolean to note whether we've set the values on the generators or not. */
private boolean pathPopulated = false;
protected Event doExecute(final RequestContext context) throws Exception
{
final HttpServletRequest request = WebUtils
.getHttpServletRequest(context);
if (!this.pathPopulated)
{
final String contextPath = context.getExternalContext()
.getContextPath();
final String cookiePath = StringUtils.hasText(contextPath) ? contextPath
: "/";
logger.info("Setting path for cookies to: " + cookiePath);
this.warnCookieGenerator.setCookiePath(cookiePath);
this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
this.pathPopulated = true;
}
String ticketGrantingTicketId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
boolean warnCookieValue = Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request));
context.getFlowScope().put("ticketGrantingTicketId", ticketGrantingTicketId);
context.getFlowScope().put("warnCookieValue", warnCookieValue);
// 存放service url
context.getFlowScope().put("serviceUrl", request.getParameter("service"));
final Service service = WebUtils.getService(this.argumentExtractors,
context);
if (service != null && logger.isDebugEnabled())
{
logger.debug("Placing service in FlowScope: " + service.getId());
}
context.getFlowScope().put("service", service);
// 客户端必须传递loginUrl参数过来,否则无法确定登陆目标页面
if (StringUtils.hasText(request.getParameter("loginUrl")))
{
String loginUrl = request.getParameter("loginUrl");
System.out.println(loginUrl);
context.getFlowScope().put("remoteLoginUrl",loginUrl);
} else
{
request.setAttribute("remoteLoginMessage",
"loginUrl parameter must be supported.");
return error();
}
// 若参数包含submit则进行提交,否则进行验证
if (StringUtils.hasText(request.getParameter("submit")))
{
return result("submit");
} else
{
Event event = result("checkTicketGrantingTicket");
return event;
}
}
public void setTicketGrantingTicketCookieGenerator(
final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator)
{
this.ticketGrantingTicketCookieGenerator = ticketGrantingTicketCookieGenerator;
}
public void setWarnCookieGenerator(
final CookieRetrievingCookieGenerator warnCookieGenerator)
{
this.warnCookieGenerator = warnCookieGenerator;
}
public void setArgumentExtractors(
final List<ArgumentExtractor> argumentExtractors)
{
this.argumentExtractors = argumentExtractors;
}
}
- 在 WEB-INF 下增加 remoteLogin-weblow.xml 自定义登录 webflow 流程文件
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"
start-state="remoteLogin">
<!-- <on-start>
<evaluate expression="remoteLogin" />
</on-start> -->
<!-- <on-start> <evaluate expression="remoteLoginAction.doBind(flowRequestContext,
flowScope.credentials)" /> </on-start> -->
<var name="credentials"
class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
<!-- 远程登陆主要Action -->
<action-state id="remoteLogin">
<evaluate expression="remoteLoginAction" />
<transition on="error" to="remoteCallbackView" />
<transition on="submit" to="bindAndValidate" />
<transition on="checkTicketGrantingTicket" to="ticketGrantingTicketExistsCheck" />
</action-state>
<!-- 远程回调页面,主要以JavaScript的方式回传一些参数用 -->
<end-state id="remoteCallbackView" view="viewStatisticsView" />
<decision-state id="ticketGrantingTicketExistsCheck">
<if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck"
else="gatewayRequestCheck" />
</decision-state>
<decision-state id="hasServiceCheck">
<if test="flowScope.service != null" then="generateServiceTicket"
else="remoteCallbackView" />
</decision-state>
<decision-state id="gatewayRequestCheck">
<if
test="externalContext.requestParameterMap['gateway'] neq '' && externalContext.requestParameterMap['gateway'] neq null && flowScope.service neq null"
then="redirect" else="remoteCallbackView" />
</decision-state>
<action-state id="bindAndValidate">
<evaluate
expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
<transition on="success" to="submit" />
<transition on="warn" to="warn" />
<transition on="error" to="remoteCallbackView" />
</action-state>
<action-state id="generateServiceTicket">
<evaluate expression="generateServiceTicketAction" />
<transition on="success" to="warn" />
<transition on="error" to="remoteCallbackView" />
<transition on="gateway" to="redirect" />
</action-state>
<decision-state id="warn">
<if test="flowScope.warnCookieValue" then="showWarningView" else="redirect" />
</decision-state>
<action-state id="submit">
<evaluate
expression="authenticationViaFormAction.submit(flowRequestContext, messageContext)" />
<transition on="warn" to="warn" />
<transition on="success" to="sendTicketGrantingTicket" />
<transition on="error" to="remoteCallbackView" />
</action-state>
<action-state id="sendTicketGrantingTicket">
<evaluate expression="sendTicketGrantingTicketAction" />
<transition to="serviceCheck" />
</action-state>
<decision-state id="serviceCheck">
<if test="flowScope.service neq null" then="generateServiceTicket"
else="remoteCallbackView" />
</decision-state>
<end-state id="showWarningView" view="casLoginConfirmView" />
<!-- <end-state id="redirect" view="bean:dynamicRedirectViewSelector" /> -->
<action-state id="redirect">
<evaluate
expression="flowScope.service.getResponse(requestScope.serviceTicketId)"
result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response" />
<transition to="postRedirectDecision" />
</action-state>
<decision-state id="postRedirectDecision">
<if test="requestScope.response.responseType.name() eq 'POST'"
then="postView" else="redirectView" />
</decision-state>
<!-- <decision-state id="hashServiceUrl">
<if test="flowScope.serviceUrl neq null" then="redirectServiceView" else="redirectView"/>
</decision-state>
<end-state id="redirectServiceView" view="externalRedirect:${flowScope.serviceUrl}" /> -->
<end-state id="postView" view="postResponseView">
<on-entry>
<set name="requestScope.parameters" value="requestScope.response.attributes" />
<set name="requestScope.originalUrl" value="flowScope.service.id" />
</on-entry>
</end-state>
<end-state id="redirectView" view="externalRedirect:${requestScope.response.url}" />
<end-state id="viewServiceErrorView" view="viewServiceErrorView" />
<end-state id="viewServiceSsoErrorView" view="viewServiceSsoErrorView" />
<global-transitions>
<transition to="viewServiceErrorView"
on-exception="org.springframework.webflow.execution.repository.NoSuchFlowExecutionException" />
<transition to="viewServiceSsoErrorView"
on-exception="org.jasig.cas.services.UnauthorizedSsoServiceException" />
<transition to="viewServiceErrorView"
on-exception="org.jasig.cas.services.UnauthorizedServiceException" />
</global-transitions>
</flow>
4. 修改 cas-servlet.xml,在后面加上一下代码
<bean id="handlerMappingB"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/remoteLogin">remoteLoginController</prop>
<prop key="/remoteLogout">remoteLogoutController</prop>
</props>
</property>
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor" />
</list>
</property>
</bean>
<bean id="remoteLoginController" class="org.springframework.webflow.mvc.servlet.FlowController">
<property name="flowExecutor" ref="remoteLoginFlowExecutor" />
<property name="flowUrlHandler" ref="flowUrlHandler" />
</bean>
<bean id="remoteLogoutController" class="org.springframework.webflow.mvc.servlet.FlowController">
<property name="flowExecutor" ref="remoteLogoutFlowExecutor" />
<property name="flowUrlHandler" ref="flowUrlHandler" />
</bean>
<webflow:flow-executor id="remoteLoginFlowExecutor"
flow-registry="remoteLoginFlowRegistry">
<webflow:flow-execution-attributes>
<webflow:always-redirect-on-pause value="false" />
</webflow:flow-execution-attributes>
</webflow:flow-executor>
<webflow:flow-executor id="remoteLogoutFlowExecutor"
flow-registry="remoteLogoutFlowRegistry">
<webflow:flow-execution-attributes>
<webflow:always-redirect-on-pause value="false" />
</webflow:flow-execution-attributes>
</webflow:flow-executor>
<webflow:flow-registry id="remoteLoginFlowRegistry"
flow-builder-services="builder">
<webflow:flow-location path="/WEB-INF/remoteLogin-webflow.xml"
id="remoteLogin"/>
</webflow:flow-registry>
<webflow:flow-registry id="remoteLogoutFlowRegistry"
flow-builder-services="builder">
<webflow:flow-location path="/WEB-INF/remoteLogout-webflow.xml"
id="remoteLogout"/>
</webflow:flow-registry>
<webflow:flow-builder-services id="flowBuilderServices"
view-factory-creator="viewFactoryCreator" />
<bean id="remoteLoginAction" class="org.jasig.cas.web.flow.RemoteLoginAction"
p:argumentExtractors-ref="argumentExtractors"
p:warnCookieGenerator-ref="warnCookieGenerator"
p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator" />
<bean id="remoteLogoutAction" class="org.jasig.cas.web.flow.RemoteLogoutAction"
p:argumentExtractors-ref="argumentExtractors"
p:warnCookieGenerator-ref="warnCookieGenerator"
p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"
p:centralAuthenticationService-ref="centralAuthenticationService"/>
5. 增加 remoteCallView.jsp 返回错误提示信息
<%@ page pageEncoding="UTF-8" %>
<%@ page contentType="text/html; charset=UTF-8" %>
<script type="text/javascript">
var remoteUrl = "${remoteLoginUrl}?validated=true";
// 构造错误消息,从webflow scope中取出
var errorMessage = '${remoteLoginMessage}';
/* <spring:hasBindErrors name="credentials">
errorMessage = "&errorMessage=" + encodeURIComponent('<c:forEach var="error" items="${errors.allErrors}"><spring:message code="${error.code}" text="${error.defaultMessage}" /></c:forEach>');
</spring:hasBindErrors> */
// 如果存在错误消息则追加到 url中
if(null != errorMessage && errorMessage.length > 0)
{
errorMessage = "&errorMessage=" + encodeURIComponent(errorMessage);
}
// 构造service
var service = "${service}";
if (service != null && service != "") {
service = "&service=" + encodeURIComponent(service);
}
// 跳转回去(客户端)
window.location.href = remoteUrl + errorMessage + service;
</script>
6. 在 default_views.properties 中新增以下配置
viewStatisticsView.(class)=org.springframework.web.servlet.view.JstlView
viewStatisticsView.url=/WEB-INF/view/jsp/default/ui/remoteCallbackView.jsp
下一篇文章我会写一下在实际开发中遇到的一些问题,供广大网友参考,在此也感谢一下很多博客主分享的文章,参考了很多的文章。