让CAS支持客户端自定义登陆页面
2014-01-02 10:47
741 查看
修改需要基于几个基本原则:
不影响原有统一登陆界面功能
客户端应尽量保持简单
尽量保证原有功能的完整性和安全性
对于第三点,必须事先说明:将登陆页面放到客户端本身就是降低了CAS安全性,这意味着作为服务向外发布的CAS服务器中的用户密码有可能由于客户端的不安全性而导致泄露,整个CAS系统成为了一个“水桶形态”,整个CAS体系的安全性将取决于所有客户端中安全性最低的一个。这也是CAS官方一直不推荐的方式。
接下来我们讲解服务器端修改的详细过程:
首先,修改/WEB-INF/web.xml,为cas增加一个/remoteLogin的映射:
<servlet-mapping>
<servlet-name>cas</servlet-name>
<url-pattern>/remoteLogin</url-pattern>
</servlet-mapping>
然后修改cas-servlet.xml文件,增加我们对/remoteLogin映射的处理,需要增加一个新流程:
<bean id=”handlerMappingB” class=”org.springframework.web.servlet.handler.SimpleUrlHandlerMapping”>
<property name=”mappings”>
<props>
<prop key=”/login”>loginController</prop>
<prop key=”/remoteLogin”>remoteLoginController</prop>
</props>
</property>
<property name=”interceptors”>
<list>
<ref bean=”localeChangeInterceptor” />
</list>
</property>
</bean>
然后在cas-servlet.xml文件中添加我们上面所配置的remoteController的bean:
<!– 增加远程控制者,允许以/remote请求启动remote控制流程 –>
<bean id=”remoteLoginController”
class=”org.springframework.webflow.executor.mvc.FlowController”
p:flowExecutor-ref=”remoteLoginFlowExecutor”
p:defaultFlowId=”remoteLogin-webflow”>
<property name=”argumentHandler”>
<bean
class=”org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler”
p:flowExecutionKeyArgumentName=”lt”
p:defaultFlowId=”remoteLogin-webflow” />
</property>
</bean>
<flow:executor id=”remoteLoginFlowExecutor” registry-ref=”remoteLoginFlowRegistry”>
<flow:execution-attributes>
<flow:alwaysRedirectOnPause value=”false”/>
</flow:execution-attributes>
</flow:executor>
<flow:registry id=”remoteLoginFlowRegistry”>
<flow:location path=”/WEB-INF/remoteLogin-webflow.xml”/>
</flow:registry>
可以看到上面将请求指向了webflow配置文件/WEB-INF/remoteLogin-webflow.xml文件,我们需要创建此文件并配置其成为我们所需的流程,以下是remoteLogin-webflow.xml全文:
<?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-1.0.xsd”>
<start-state idref=”remoteLogin”/>
<!– 远程登陆主要Action –>
<action-state id=”remoteLogin”>
<action bean=”remoteLoginAction” />
<transition on=”error” to=”remoteCallbackView” />
<transition on=”submit” to=”bindAndValidate” />
<transition on=”checkTicketGrantingTicket” to=”ticketGrantingTicketExistsCheck” />
</action-state>
<!– 远程回调页面,主要以JavaScript的方式回传一些参数用 –>
<end-state id=”remoteCallbackView” view=”remoteCallbackView” />
<decision-state id=”ticketGrantingTicketExistsCheck”>
<if test=”${flowScope.ticketGrantingTicketId != null}” then=”hasServiceCheck”
else=”gatewayRequestCheck” />
</decision-state>
<decision-state id=”gatewayRequestCheck”>
<if test=”${externalContext.requestParameterMap['gateway'] != ” && externalContext.requestParameterMap['gateway'] != null && flowScope.service != null}” then=”redirect” else=”remoteCallbackView” />
</decision-state>
<decision-state id=”hasServiceCheck”>
<if test=”${flowScope.service != null}” then=”generateServiceTicket” else=”remoteCallbackView” />
</decision-state>
<!–
The “warn” action makes the determination of whether to redirect directly to the
requested
service or display the “confirmation” page to go back to the server.
–>
<decision-state id=”warn”>
<if test=”${flowScope.warnCookieValue}” then=”showWarningView” else=”redirect” />
</decision-state>
<action-state id=”bindAndValidate”>
<action bean=”authenticationViaFormAction” />
<transition on=”success” to=”submit” />
<transition on=”error” to=”remoteCallbackView” />
</action-state>
<action-state id=”submit”>
<action bean=”authenticationViaFormAction” method=”submit” />
<transition on=”warn” to=”warn” />
<transition on=”success” to=”sendTicketGrantingTicket” />
<transition on=”error” to=”remoteCallbackView” />
</action-state>
<action-state id=”sendTicketGrantingTicket”>
<action bean=”sendTicketGrantingTicketAction” />
<transition on=”success” to=”serviceCheck” />
</action-state>
<decision-state id=”serviceCheck”>
<if test=”${flowScope.service != null}” then=”generateServiceTicket”
else=”remoteCallbackView” />
</decision-state>
<action-state id=”generateServiceTicket”>
<action bean=”generateServiceTicketAction” />
<transition on=”success” to =”warn” />
<transition on=”error” to=”remoteCallbackView” />
<transition on=”gateway” to=”redirect” />
</action-state>
<!–
The “showWarningView” end state is the end state for when the user has requested
privacy settings (to be “warned”) to be turned on. It delegates to a
view defines in default_views.properties that display the “Please click here to go
to the service.” message.
–>
<end-state id=”showWarningView” view=”casLoginConfirmView” />
<!–
The “redirect” end state allows CAS to properly end the workflow while still
redirecting
the user back to the service required.
–>
<end-state id=”redirect” view=”bean:dynamicRedirectViewSelector” />
<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>
以上文件根据原login-webflow.xml文件修改,粗体为修改部分。可以看到,我们在流程中增加了remoteLogin Action节点和remoteCallback View节点,下面我们配置remoteLogin节点:
在/WEB-INF/cas-servlet.xml文件中增加remoteLoginAction配置:
<bean id=”remoteLoginAction”
class=”com.baidu.cas.web.flow.RemoteLoginAction”
p:argumentExtractors-ref=”argumentExtractors”
p:warnCookieGenerator-ref=”warnCookieGenerator”
p:ticketGrantingTicketCookieGenerator-ref=”ticketGrantingTicketCookieGenerator” />
同时创建com.baidu.cas.web.flow.RemoteLoginAction类:
/**
* 远程登陆票据提供Action.
* 根据InitialFlowSetupAction修改.
* 由于InitialFlowSetupAction为final类,因此只能将代码复制过来再进行修改.
*
* @author GuoLin
*/
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. */
@NotEmpty
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;
}
context.getFlowScope().put(“ticketGrantingTicketId”,
this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
context.getFlowScope().put(“warnCookieValue”,
Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
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”))) {
context.getFlowScope().put(“remoteLoginUrl”, request.getParameter(“loginUrl”));
} else {
request.setAttribute(“remoteLoginMessage”, “loginUrl parameter must be supported.”);
return error();
}
// 若参数包含submit则进行提交,否则进行验证
if (StringUtils.hasText(request.getParameter(“submit”))) {
return result(“submit”);
} else {
return result(“checkTicketGrantingTicket”);
}
}
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;
}
}
以上粗体为修改部分,要求客户端必须传入loginUrl参数,且当客户端传入submit参数时,直接为其提交用户名密码
然后再配置remoteCallbackView显示节点,修改src/default_views.properties文件,增加remoteCallbackView配置:
### 配置远程回调页面
remoteCallbackView.(class)=org.springframework.web.servlet.view.JstlView
remoteCallbackView.url=/WEB-INF/view/jsp/default/ui/remoteCallbackView.jsp
创建/WEB-INF/view/jsp/default/ui/remoteCallbackView.jsp文件:
<%@ page language=”java” contentType=”text/html; charset=utf-8″ pageEncoding=”utf-8″%>
<%@ taglib prefix=”c” uri=”http://java.sun.com/jsp/jstl/core”%>
<%@ taglib prefix=”spring” uri=”http://www.springframework.org/tags”%>
<html>
<head>
<script type=”text/javascript”>
var remoteUrl = “${remoteLoginUrl}?validated=true”;
// 构造错误消息
var errorMessage = “”;
<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>
// 构造service
var service = “”;
<c:if test=”${service != null && service != ”}”>
service = “&service=” + encodeURIComponent(“${service}”);
</c:if>
// 跳转回去
window.location.href = remoteUrl + errorMessage + service;
</script>
</head>
<body>
${remoteLoginMessage}
</body>
</html>
以上文件注意粗体部分validated=true,这里我们与客户端约定,当客户端登陆页面后带有参数validated=true时,不进行票据认证请求。这是因为,客户端登陆页面为http://clienthost/login.jsp,那么当用户访问URL http://clienthost/login.jsp时,客户端会重定向到CAS中央服务器请求TGT认证,但认证失败后CAS中央认证服务器会重定向到客户端登陆页面并显示登陆框,此时客户端必须以某种规则避免重新请求中央认证服务器认证, 在这里我们与客户端约定,当回发的请求为登陆页面且带有参数validated=true时即不转发TGT认证请求,即 http://
clienthost/login.jsp?validated=true 请求客户端不会重新发送TGT认证请求给中央认证服务器
到此,服务器端修改完成,下一篇介绍客户端如何构建
客户端即指使用CAS中央认证服务器的应用程序,而不是指用户浏览器
客户端实现目标
客户端实现主要需要满足5个case:
1. 用户未在中央认证服务器登陆,访问客户端受保护资源时,客户端重定向到中央认证服务器请求TGT认证,认证失败,转回客户端登陆页面,保证受保护资源URL信息不丢失
2. 用户未在中央认证服务器登陆,访问客户端登陆页面时,客户端重定向到中央认证服务器请求TGT认证,认证失败,转回客户端登陆页面,此次登录页面不再受保护,允许访问
3. 用户已在中央认证服务器登陆,访问客户端受保护资源时,客户端重定向到中央认证服务器请求TGT认证,认证成功,直接转回受保护资源
4. 用户在客户端登陆页面提交用户名密码,客户端将用户名密码信息提交给服务器端,认证失败,转回客户端登陆页面,携带失败信息并保证转到登陆页面前受保护资源URL信息不丢失
5. 用户在客户端登陆页面提交用户名密码,客户端将用户名密码信息提交给服务器端,认证成功,转回转到登陆页面前受保护资源
对于case 1和case 3,普通的CAS客户端即可满足需求,但对于case 4和case 5,则需要我们定制自己的登陆页面。对于case 2,主要是需要满足部分登陆页面希望在用户未登陆状态显示登陆框,在已登陆状态显示用户欢迎信息的需求,实现这个需求我们是通过让CAS客户端认证器满足 一个排除约定,即当用户请求路径为登陆页面且带有validated=true的参数时,即不进行重定向TGT认证请求
客户端修改方案
远程客户端修改,对于任何一种客户端方案都可以实现,这里为了简单起见,我们给出的修改方案基于CAS官方提供的Java客户端3.1.3。首先我们使用CAS Client 3.1.3搭建一个CAS客户端,具体搭建方法可以参考CAS官网:CAS Client for Java 3.1
根据服务器流程修改方案,我们可以知道,所有的远程请求都必须携带有loginUrl参数信息以使得服务器端知道在认证失败后转向客户端登陆页面。 而在CAS客户端上,上一节的case 4和case 5,我们主要通过提交表单的方式传递loginUrl,而case 1, case 3则是依靠org.jasig.cas.client.authentication.AuthenticationFilter类进行的转向,但使用 AuthenticationFilter转向时,是没有loginUrl信息的,因此我们首先需要重新实现一个自己的认证过滤器,以下是我们自己的认证
过滤器的代码:
/**
* 远程认证过滤器.
* 由于AuthenticationFilter的doFilter方法被声明为final,
* 只好重新实现一个认证过滤器,支持localLoginUrl设置.
*
* @author GuoLin
*
*/
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.”);
}
public final void doFilter(final ServletRequest servletRequest,
final ServletResponse servletResponse, final 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;
}
}
以上黄色背景代码为修改部分,其余代码均拷贝自 org.jasig.cas.client.authentication.AuthenticationFilter,可以看到我们为原有的认证过滤器 增加了一个参数localLoginUrl。在WEB-INF/web.xml中配置:
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>com.baidu.cas.client.validation.RemoteAuthenticationFilter</filter-
class>
<init-param>
<param-name>localLoginUrl</param-name>
<param-value>http://GUOLIN:9080/cas-client-java-custom-login/login.jsp</param-
value>
</init-param>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>https://GUOLIN/cas-server/remoteLogin</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://GUOLIN:9080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
此处我们将过滤器指向自己的过滤器并增加本地登陆页面路径设置。最后我们来看看登陆页面login.jsp:
<%@ page language=”java” contentType=”text/html; charset=utf-8″ pageEncoding=”utf-8″%>
<!DOCTYPE html PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN” “http://www.w3.org/TR/html4/loose.dtd”>
<head>
<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″>
<title>远程CAS客户端登陆页面</title>
<link rel=”stylesheet” type=”text/css” href=”http://blog.163.com/wm_at163/blog/<%= request.getContextPath() %>/styles/main.css” />
<script type=”text/javascript”>
function getParam(name) {
var queryString = window.location.search;
var param = queryString.substr(1, query.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) {
document.getElementById(“errorMessage”).innerHTML = decodeURIComponent(error);
}
// 注入service
var service = getParam(“service”);
if (service)
document.getElementById(“service”).value = decodeURIComponent(service);
else
document.getElementById(“service”).value = location.href;
}
</script>
</head>
<body>
<h1>远程CAS客户端登陆页面</h1>
<% if (request.getRemoteUser() == null) { %>
<div id=”errorMessage”></div>
<form id=”myLoginForm” action=”https://guolin/cas-server/remoteLogin” method=”post”>
<input type=”hidden” id=”service” name=”service” value=”">
<input type=”hidden” name=”loginUrl” value=”http://guolin:9080/cas-client-java-custom-login/login.jsp”>
<input type=”hidden” name=”submit” value=”true” />
<table>
<tr>
<td>用户名:</td>
<td><input type=”text” name=”username”></td>
</tr>
<tr>
<td>密 码:</td>
<td><input type=”password” name=”password”></td>
</tr>
<tr>
<td colspan=”2″><input type=”submit” value=”登陆” /></td>
</tr>
</table>
</form>
<script type=”text/javascript”>init()</script>
<% } else { %>
<div class=”welcome”>您好:<%= request.getRemoteUser() %></div>
<div id=”logout”>
<a href=”http://blog.163.com/wm_at163/blog/https://GUOLIN/cas-server/remoteLogout?service=http://guolin:9080/cas-client-java-custom-login/login.jsp”>单点登出</a>
</div>
<% } %>
</body>
</html>
login.jsp主要是为了满足上一节中的case 4和case 5,这里为了简单起见,仅有是否已登陆判断使用了服务器端代码,其他均使用客户端代码实现。以上黄色背景字中,我们首先将表单action指向服务器端 remoteLogin,然后在里面设置了两个重要的hidden域以传递 loginUrl和submit参数,前者用于告诉服务器失败后转向何处,后者告诉服务器端webflow现在要进行提交而不是TGT认证请求
不影响原有统一登陆界面功能
客户端应尽量保持简单
尽量保证原有功能的完整性和安全性
对于第三点,必须事先说明:将登陆页面放到客户端本身就是降低了CAS安全性,这意味着作为服务向外发布的CAS服务器中的用户密码有可能由于客户端的不安全性而导致泄露,整个CAS系统成为了一个“水桶形态”,整个CAS体系的安全性将取决于所有客户端中安全性最低的一个。这也是CAS官方一直不推荐的方式。
接下来我们讲解服务器端修改的详细过程:
首先,修改/WEB-INF/web.xml,为cas增加一个/remoteLogin的映射:
<servlet-mapping>
<servlet-name>cas</servlet-name>
<url-pattern>/remoteLogin</url-pattern>
</servlet-mapping>
然后修改cas-servlet.xml文件,增加我们对/remoteLogin映射的处理,需要增加一个新流程:
<bean id=”handlerMappingB” class=”org.springframework.web.servlet.handler.SimpleUrlHandlerMapping”>
<property name=”mappings”>
<props>
<prop key=”/login”>loginController</prop>
<prop key=”/remoteLogin”>remoteLoginController</prop>
</props>
</property>
<property name=”interceptors”>
<list>
<ref bean=”localeChangeInterceptor” />
</list>
</property>
</bean>
然后在cas-servlet.xml文件中添加我们上面所配置的remoteController的bean:
<!– 增加远程控制者,允许以/remote请求启动remote控制流程 –>
<bean id=”remoteLoginController”
class=”org.springframework.webflow.executor.mvc.FlowController”
p:flowExecutor-ref=”remoteLoginFlowExecutor”
p:defaultFlowId=”remoteLogin-webflow”>
<property name=”argumentHandler”>
<bean
class=”org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler”
p:flowExecutionKeyArgumentName=”lt”
p:defaultFlowId=”remoteLogin-webflow” />
</property>
</bean>
<flow:executor id=”remoteLoginFlowExecutor” registry-ref=”remoteLoginFlowRegistry”>
<flow:execution-attributes>
<flow:alwaysRedirectOnPause value=”false”/>
</flow:execution-attributes>
</flow:executor>
<flow:registry id=”remoteLoginFlowRegistry”>
<flow:location path=”/WEB-INF/remoteLogin-webflow.xml”/>
</flow:registry>
可以看到上面将请求指向了webflow配置文件/WEB-INF/remoteLogin-webflow.xml文件,我们需要创建此文件并配置其成为我们所需的流程,以下是remoteLogin-webflow.xml全文:
<?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-1.0.xsd”>
<start-state idref=”remoteLogin”/>
<!– 远程登陆主要Action –>
<action-state id=”remoteLogin”>
<action bean=”remoteLoginAction” />
<transition on=”error” to=”remoteCallbackView” />
<transition on=”submit” to=”bindAndValidate” />
<transition on=”checkTicketGrantingTicket” to=”ticketGrantingTicketExistsCheck” />
</action-state>
<!– 远程回调页面,主要以JavaScript的方式回传一些参数用 –>
<end-state id=”remoteCallbackView” view=”remoteCallbackView” />
<decision-state id=”ticketGrantingTicketExistsCheck”>
<if test=”${flowScope.ticketGrantingTicketId != null}” then=”hasServiceCheck”
else=”gatewayRequestCheck” />
</decision-state>
<decision-state id=”gatewayRequestCheck”>
<if test=”${externalContext.requestParameterMap['gateway'] != ” && externalContext.requestParameterMap['gateway'] != null && flowScope.service != null}” then=”redirect” else=”remoteCallbackView” />
</decision-state>
<decision-state id=”hasServiceCheck”>
<if test=”${flowScope.service != null}” then=”generateServiceTicket” else=”remoteCallbackView” />
</decision-state>
<!–
The “warn” action makes the determination of whether to redirect directly to the
requested
service or display the “confirmation” page to go back to the server.
–>
<decision-state id=”warn”>
<if test=”${flowScope.warnCookieValue}” then=”showWarningView” else=”redirect” />
</decision-state>
<action-state id=”bindAndValidate”>
<action bean=”authenticationViaFormAction” />
<transition on=”success” to=”submit” />
<transition on=”error” to=”remoteCallbackView” />
</action-state>
<action-state id=”submit”>
<action bean=”authenticationViaFormAction” method=”submit” />
<transition on=”warn” to=”warn” />
<transition on=”success” to=”sendTicketGrantingTicket” />
<transition on=”error” to=”remoteCallbackView” />
</action-state>
<action-state id=”sendTicketGrantingTicket”>
<action bean=”sendTicketGrantingTicketAction” />
<transition on=”success” to=”serviceCheck” />
</action-state>
<decision-state id=”serviceCheck”>
<if test=”${flowScope.service != null}” then=”generateServiceTicket”
else=”remoteCallbackView” />
</decision-state>
<action-state id=”generateServiceTicket”>
<action bean=”generateServiceTicketAction” />
<transition on=”success” to =”warn” />
<transition on=”error” to=”remoteCallbackView” />
<transition on=”gateway” to=”redirect” />
</action-state>
<!–
The “showWarningView” end state is the end state for when the user has requested
privacy settings (to be “warned”) to be turned on. It delegates to a
view defines in default_views.properties that display the “Please click here to go
to the service.” message.
–>
<end-state id=”showWarningView” view=”casLoginConfirmView” />
<!–
The “redirect” end state allows CAS to properly end the workflow while still
redirecting
the user back to the service required.
–>
<end-state id=”redirect” view=”bean:dynamicRedirectViewSelector” />
<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>
以上文件根据原login-webflow.xml文件修改,粗体为修改部分。可以看到,我们在流程中增加了remoteLogin Action节点和remoteCallback View节点,下面我们配置remoteLogin节点:
在/WEB-INF/cas-servlet.xml文件中增加remoteLoginAction配置:
<bean id=”remoteLoginAction”
class=”com.baidu.cas.web.flow.RemoteLoginAction”
p:argumentExtractors-ref=”argumentExtractors”
p:warnCookieGenerator-ref=”warnCookieGenerator”
p:ticketGrantingTicketCookieGenerator-ref=”ticketGrantingTicketCookieGenerator” />
同时创建com.baidu.cas.web.flow.RemoteLoginAction类:
/**
* 远程登陆票据提供Action.
* 根据InitialFlowSetupAction修改.
* 由于InitialFlowSetupAction为final类,因此只能将代码复制过来再进行修改.
*
* @author GuoLin
*/
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. */
@NotEmpty
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;
}
context.getFlowScope().put(“ticketGrantingTicketId”,
this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
context.getFlowScope().put(“warnCookieValue”,
Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
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”))) {
context.getFlowScope().put(“remoteLoginUrl”, request.getParameter(“loginUrl”));
} else {
request.setAttribute(“remoteLoginMessage”, “loginUrl parameter must be supported.”);
return error();
}
// 若参数包含submit则进行提交,否则进行验证
if (StringUtils.hasText(request.getParameter(“submit”))) {
return result(“submit”);
} else {
return result(“checkTicketGrantingTicket”);
}
}
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;
}
}
以上粗体为修改部分,要求客户端必须传入loginUrl参数,且当客户端传入submit参数时,直接为其提交用户名密码
然后再配置remoteCallbackView显示节点,修改src/default_views.properties文件,增加remoteCallbackView配置:
### 配置远程回调页面
remoteCallbackView.(class)=org.springframework.web.servlet.view.JstlView
remoteCallbackView.url=/WEB-INF/view/jsp/default/ui/remoteCallbackView.jsp
创建/WEB-INF/view/jsp/default/ui/remoteCallbackView.jsp文件:
<%@ page language=”java” contentType=”text/html; charset=utf-8″ pageEncoding=”utf-8″%>
<%@ taglib prefix=”c” uri=”http://java.sun.com/jsp/jstl/core”%>
<%@ taglib prefix=”spring” uri=”http://www.springframework.org/tags”%>
<html>
<head>
<script type=”text/javascript”>
var remoteUrl = “${remoteLoginUrl}?validated=true”;
// 构造错误消息
var errorMessage = “”;
<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>
// 构造service
var service = “”;
<c:if test=”${service != null && service != ”}”>
service = “&service=” + encodeURIComponent(“${service}”);
</c:if>
// 跳转回去
window.location.href = remoteUrl + errorMessage + service;
</script>
</head>
<body>
${remoteLoginMessage}
</body>
</html>
以上文件注意粗体部分validated=true,这里我们与客户端约定,当客户端登陆页面后带有参数validated=true时,不进行票据认证请求。这是因为,客户端登陆页面为http://clienthost/login.jsp,那么当用户访问URL http://clienthost/login.jsp时,客户端会重定向到CAS中央服务器请求TGT认证,但认证失败后CAS中央认证服务器会重定向到客户端登陆页面并显示登陆框,此时客户端必须以某种规则避免重新请求中央认证服务器认证, 在这里我们与客户端约定,当回发的请求为登陆页面且带有参数validated=true时即不转发TGT认证请求,即 http://
clienthost/login.jsp?validated=true 请求客户端不会重新发送TGT认证请求给中央认证服务器
到此,服务器端修改完成,下一篇介绍客户端如何构建
客户端即指使用CAS中央认证服务器的应用程序,而不是指用户浏览器
客户端实现目标
客户端实现主要需要满足5个case:
1. 用户未在中央认证服务器登陆,访问客户端受保护资源时,客户端重定向到中央认证服务器请求TGT认证,认证失败,转回客户端登陆页面,保证受保护资源URL信息不丢失
2. 用户未在中央认证服务器登陆,访问客户端登陆页面时,客户端重定向到中央认证服务器请求TGT认证,认证失败,转回客户端登陆页面,此次登录页面不再受保护,允许访问
3. 用户已在中央认证服务器登陆,访问客户端受保护资源时,客户端重定向到中央认证服务器请求TGT认证,认证成功,直接转回受保护资源
4. 用户在客户端登陆页面提交用户名密码,客户端将用户名密码信息提交给服务器端,认证失败,转回客户端登陆页面,携带失败信息并保证转到登陆页面前受保护资源URL信息不丢失
5. 用户在客户端登陆页面提交用户名密码,客户端将用户名密码信息提交给服务器端,认证成功,转回转到登陆页面前受保护资源
对于case 1和case 3,普通的CAS客户端即可满足需求,但对于case 4和case 5,则需要我们定制自己的登陆页面。对于case 2,主要是需要满足部分登陆页面希望在用户未登陆状态显示登陆框,在已登陆状态显示用户欢迎信息的需求,实现这个需求我们是通过让CAS客户端认证器满足 一个排除约定,即当用户请求路径为登陆页面且带有validated=true的参数时,即不进行重定向TGT认证请求
客户端修改方案
远程客户端修改,对于任何一种客户端方案都可以实现,这里为了简单起见,我们给出的修改方案基于CAS官方提供的Java客户端3.1.3。首先我们使用CAS Client 3.1.3搭建一个CAS客户端,具体搭建方法可以参考CAS官网:CAS Client for Java 3.1
根据服务器流程修改方案,我们可以知道,所有的远程请求都必须携带有loginUrl参数信息以使得服务器端知道在认证失败后转向客户端登陆页面。 而在CAS客户端上,上一节的case 4和case 5,我们主要通过提交表单的方式传递loginUrl,而case 1, case 3则是依靠org.jasig.cas.client.authentication.AuthenticationFilter类进行的转向,但使用 AuthenticationFilter转向时,是没有loginUrl信息的,因此我们首先需要重新实现一个自己的认证过滤器,以下是我们自己的认证
过滤器的代码:
/**
* 远程认证过滤器.
* 由于AuthenticationFilter的doFilter方法被声明为final,
* 只好重新实现一个认证过滤器,支持localLoginUrl设置.
*
* @author GuoLin
*
*/
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.”);
}
public final void doFilter(final ServletRequest servletRequest,
final ServletResponse servletResponse, final 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;
}
}
以上黄色背景代码为修改部分,其余代码均拷贝自 org.jasig.cas.client.authentication.AuthenticationFilter,可以看到我们为原有的认证过滤器 增加了一个参数localLoginUrl。在WEB-INF/web.xml中配置:
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>com.baidu.cas.client.validation.RemoteAuthenticationFilter</filter-
class>
<init-param>
<param-name>localLoginUrl</param-name>
<param-value>http://GUOLIN:9080/cas-client-java-custom-login/login.jsp</param-
value>
</init-param>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>https://GUOLIN/cas-server/remoteLogin</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://GUOLIN:9080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
此处我们将过滤器指向自己的过滤器并增加本地登陆页面路径设置。最后我们来看看登陆页面login.jsp:
<%@ page language=”java” contentType=”text/html; charset=utf-8″ pageEncoding=”utf-8″%>
<!DOCTYPE html PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN” “http://www.w3.org/TR/html4/loose.dtd”>
<head>
<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″>
<title>远程CAS客户端登陆页面</title>
<link rel=”stylesheet” type=”text/css” href=”http://blog.163.com/wm_at163/blog/<%= request.getContextPath() %>/styles/main.css” />
<script type=”text/javascript”>
function getParam(name) {
var queryString = window.location.search;
var param = queryString.substr(1, query.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) {
document.getElementById(“errorMessage”).innerHTML = decodeURIComponent(error);
}
// 注入service
var service = getParam(“service”);
if (service)
document.getElementById(“service”).value = decodeURIComponent(service);
else
document.getElementById(“service”).value = location.href;
}
</script>
</head>
<body>
<h1>远程CAS客户端登陆页面</h1>
<% if (request.getRemoteUser() == null) { %>
<div id=”errorMessage”></div>
<form id=”myLoginForm” action=”https://guolin/cas-server/remoteLogin” method=”post”>
<input type=”hidden” id=”service” name=”service” value=”">
<input type=”hidden” name=”loginUrl” value=”http://guolin:9080/cas-client-java-custom-login/login.jsp”>
<input type=”hidden” name=”submit” value=”true” />
<table>
<tr>
<td>用户名:</td>
<td><input type=”text” name=”username”></td>
</tr>
<tr>
<td>密 码:</td>
<td><input type=”password” name=”password”></td>
</tr>
<tr>
<td colspan=”2″><input type=”submit” value=”登陆” /></td>
</tr>
</table>
</form>
<script type=”text/javascript”>init()</script>
<% } else { %>
<div class=”welcome”>您好:<%= request.getRemoteUser() %></div>
<div id=”logout”>
<a href=”http://blog.163.com/wm_at163/blog/https://GUOLIN/cas-server/remoteLogout?service=http://guolin:9080/cas-client-java-custom-login/login.jsp”>单点登出</a>
</div>
<% } %>
</body>
</html>
login.jsp主要是为了满足上一节中的case 4和case 5,这里为了简单起见,仅有是否已登陆判断使用了服务器端代码,其他均使用客户端代码实现。以上黄色背景字中,我们首先将表单action指向服务器端 remoteLogin,然后在里面设置了两个重要的hidden域以传递 loginUrl和submit参数,前者用于告诉服务器失败后转向何处,后者告诉服务器端webflow现在要进行提交而不是TGT认证请求
相关文章推荐
- 让CAS支持客户端自定义登陆页面
- 让CAS支持客户端自定义登陆页面——原理篇
- 让CAS支持客户端自定义登陆页面——服务器篇
- 让CAS支持客户端自定义登陆页面——客户端篇
- net控件中数据导到Excel的格式 首先,我们了解一下excel从web页面上导出的原理。当我们把这些数据发送到客户端时,我们想让客户端程序(浏览器)以excel的格式读取它,所以把mime类型设为:application/vnd.ms-excel,当excel读取文件时会以每个cell的格式呈现数据,如果cell没有规定的格式,则excel会以默认的格式去呈现该cell的数据。这样就给我们提供了自定义数据格式的空间,当然我们必须使用excel支持的格式。下面就列出常用的一些格式: 1) 文本
- CAS客户端使用Ajax登陆(即保留原有客户端登录页面)
- CAS 客户端自定义用户登陆界面
- 改造CAS单点登录 --- 自定义登陆页面(客户端)
- 自定义SharePoint的登陆页面
- Yale CAS + .net Client 实现 SSO(6)--自定义登录页面
- 【CAS】自定义登录页面,返回更多信息
- atlas客户端页面周期事件及实践:自定义错误
- 独立部署cas服务器以测试客户端各应用程序统一的单点登陆认证功能
- goahead 的认证和自定义登陆页面的cookie使用
- spring security2学习笔记二(自定义数据结构及登陆页面)
- 让dedecms搜索页面支持标签调用及自定义字段调用的方法
- 自定义Sharepoint的登陆页面
- OBIEE自定义登陆和注销页面
- 怎么让dedecms搜索页面支持标签调用及自定义字段调用
- 自定义cas客户端核心过滤器AuthenticationFilter