单点登录CAS原理

sweetbaybe

贡献于2011-07-06

字数:18876 关键词: OpenID/单点登录SSO 方案 HTML Java JavaScript

基本原理 首先,在我们修改之间,先了解以下CAS运行基本原理。CAS服务器,客户端(应用),浏览器的序列图如下: 其中: ST:Service Ticket,用于客户端应用持有,每个ST对应一个用户在一个客户端上 TGT:Ticket Granting Ticket,存储在CAS服务器端和用户cookie两个地方 CAS服务器持有ST与TGT+客户端的映射关系,客户端持有ST与用户Session的映射关系,在renew的情况下,每次客户端根据用户 Session将ST发送给CAS服务器端,服务器端检验ST是否存在即可知道此用户是否已登陆。在普通情况下,用户第一次登陆应用时,客户端将用户页面 重定向到CAS服务器,服务器取出用户cookie中的TGT,检验是否在服务器中存在,若存在则生成ST返回给客户端  (若不存在则要求登陆,登陆成功后同样返回ST给客户端),客户端拿到ST后再发送给CAS服务器认证是否为真实ST,认证成功即表示登陆成功 我们可以看到,其实我们需要做的就是第2步中返回的登陆页面由服务器改放到客户端,然后让第3步中由用户在客户端上输入用户名密码但提交到CAS服务器端,登陆成功与失败都将转向客户端。 服务器详细登陆流程 对于上一节讲述的整体登陆流程,CAS  3.3.1服务器端上是依赖于Spring  Webflow  1.0.3实现的,其主要流程在/WEB-INF/login-webflow.xml中配置,配置的页面流活动图如下(有删节): 图中命名均按照webflow配置文件中的命名,图解如下: Action State图标表示webflow配置文件中的action-state或view-state节点 Decision图标表示webflow配置文件中的decision-state节点 Initial State图标表示webflow配置文件中的start-state节点 Final State图标表示webflow配置文件中的end-state节点 登陆的流程依照图上说明,在此不再累述,下面简单说明下CAS服务器端Spring Webflow的运作 首先CAS在/WEB-INF/web.xml中配置命名为cas的servlet以拦截输入请求,若不在cas servlet mapping范围内的资源路径请求均转向到/login上:     cas     org.jasig.cas.web.init.SafeDispatcherServlet             publishContext         false         1     cas     /login ... 所有映射到cas  servlet上的请求都将经过/WEB-INF/cas-servlet.xml检查确定进入哪个Action,cas-servlet.xml中最重要的两个bean就是handlerMappingB和handlerMappingC handlerMappingB配置了登陆流程进入的路径映射,而handlerMappingC则配置了其他的流程的路径映射。/WEB-INF/login-webflow.xml流程配置文件即是在handlerMappingB中通过/login映射进入的。 Webflow依据一个生成的flowExecutionKey来确定一个流程实例走到了哪一步,每次页面流程运转总是需要提交这个 flowExecutionKey来告诉webflow它是从流程的哪个位置出发的有了以上理论作为依据,我们在下一节就可以根据自己的需要修改流程,使 之支持远程登录了 服务器登陆流程修改目标 修改后的登陆流程活动图如下: 图中橙色为我们修改的流程节点,这里我们增加了一个开始节点remoteLogin和一个结束节点 remoteCallbackView,删除了原有的loginFormView节点、  viewGenericLoginSuccess以及 renew节点(renew节点由于系统无此需求而删除),然后将所有这些节点的转向全部都转向 到remoteCallbackView节点,因为登陆和显示登陆成功信息都应该是客户端完成的 服务器端实现目标 好了,原理到这里已经啰嗦完了,下一节讲如何着手修改CAS服务器端啦 。 修改需要基于几个基本原则: 不影响原有统一登陆界面功能 客户端应尽量保持简单 尽量保证原有功能的完整性和安全性 对于第三点,必须事先说明:将登陆页面放到客户端本身就是降低了CAS安全性,这意味着作为服务向外发布的CAS服务器中的用户密码有可能由于客户端的不安全性而导致泄露,整个CAS系统成为了一个“水桶形态”,整个CAS体系的安全性将取决于所有客户端中安全性最低的一个。这也是CAS官方一直不推荐的方式。 服务器端修改 接下来我们讲解服务器端修改的详细过程: 首先,修改/WEB-INF/web.xml,为cas增加一个/remoteLogin的映射:     cas     /remoteLogin 然后修改cas-servlet.xml文件,增加我们对/remoteLogin映射的处理,需要增加一个新流程:                         loginController             remoteController                                                 然后在cas-servlet.xml文件中添加我们上面所配置的remoteController的bean:                                     可以看到上面将请求指向了webflow配置文件/WEB-INF/remoteLogin-webflow.xml文件,我们需要创建此文件并配置其成为我们所需的流程,以下是remoteLogin-webflow.xml全文:                                                                                                                                                                                                                                                                                                                                                                             以上文件根据原login-webflow.xml文件修改,黄色背景为修改部分。可以看到,我们在流程中增加了remoteLogin Action节点和remoteCallback View节点,下面我们配置remoteLogin节点: 在/WEB-INF/cas-servlet.xml文件中增加remoteLoginAction配置: 同时创建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 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 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"%>         ${remoteLoginMessage} 以上文件注意黄色背景部分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认证请求给中央认证服务器 到此,服务器端修改完成,下一篇介绍客户端如何构建 客户端实现目标 客户端实现主要需要满足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中配置:     CAS Authentication Filter     com.baidu.cas.client.validation.RemoteAuthenticationFilter             localLoginUrl         http://GUOLIN:9080/cas-client-java-custom-login/login.jsp                 casServerLoginUrl         https://GUOLIN/cas-server/remoteLogin                 serverName         http://GUOLIN:9080         CAS Authentication Filter     /* 此处我们将过滤器指向自己的过滤器并增加本地登陆页面路径设置。最后我们来看看登陆页面login.jsp: <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>         远程CAS客户端登陆页面            

远程CAS客户端登陆页面

    <% if (request.getRemoteUser() == null) { %>        
       
                                                                                                                                                                                                                                                               
用户名:
密  码:
       
            <% } else { %>        
您好:<%= request.getRemoteUser() %>
       
            单点登出        
    <% } %> login.jsp主要是为了满足上一节中的case 4和case 5,这里为了简单起见,仅有是否已登陆判断使用了服务器端代码,其他均使用客户端代码实现。以上黄色背景字中,我们首先将表单action指向服务器端 remoteLogin,然后在里面设置了两个重要的hidden域以传递  loginUrl和submit参数,前者用于告诉服务器失败后转向何处,后者告诉服务器端webflow现在要进行提交而不是TGT认证请求 这样我们的自定义客户端远程登陆页面就完成了,现在赶快测试一下吧,哇哈哈 等下次有空再吧iframe实现方案放出来啦 

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享文档获得金币 ]
23 人已下载

下载文档

相关文档