您的位置:首页 > 运维架构 > Nginx

让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认证请求
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  nginx
相关文章推荐