• CAS小总结


    主要参考
    粗略的解释:
    1. 客户登录 www.xn.com/1.html,(假设所有的资源,都会被filter拦截,这里会引发一些思考, 如果静态资源不拦截, AJAX的动态请求,如果在没有session的情况下, 会被跨域重定向到https://login.xn.com/login上,肯定会返回response空的情况,会给前端带来问题), 首先进入 www.xn.com应用的 AuthenticationFilter > doFilter ,从代码里看,如果 Assertion 是session里的一个验证实体, 在登录成功后,会被设置到session里.
    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 Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; // 判断是否登录过,如果已经登录过,进入if并且退出 if (assertion != null) { filterChain.doFilter(request, response); return; } // 如果没有登录过,继续后续处理 // 构造访问的URL,如果该Url包含tikicet参数,则去除参数 final String serviceUrl = constructServiceUrl(request, response); // 如果ticket存在,则获取URL后面的参数ticket final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName()); // 研究中 final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); // 如果ticket存在 if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { filterChain.doFilter(request, response); return; } final String modifiedServiceUrl; log.debug("no ticket and no assertion found"); if (this.gateway) { log.debug("setting gateway attribute in session"); modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl); } else { modifiedServiceUrl = serviceUrl; } if (log.isDebugEnabled()) { log.debug("Constructed service url: " + modifiedServiceUrl); } // 如果用户没有登录过,那么构造重定向的URL final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); if (log.isDebugEnabled()) { log.debug("redirecting to "" + urlToRedirectTo + """); } // 重定向跳转到Cas认证中心 response.sendRedirect(urlToRedirectTo); }
    大概意思,就是如果确认没有登录(url中没有ticket,并且 session也没有或者session里没有验证实体)就会被重定向到 https://login.xn.com/login?service=http://www.xn.com/1.html
    2. 浏览器收到重定向的response,就会访问 https://login.xn.com/login?service=http://www.xn.com/1.html , CAS SERVER会 从 login.xn.com域的 cookie 检查是否有 CASTGC,并获取一下service里的URL,会发现没有 CASTGC,所以,是个没登录的用户,就会出现登录页,用户会输入并验证.
    如果验证通过, 会创建TGT(就是TGC对应在 CAS SERVER 上的实体,这里,应该会有TGT<->Service的对应),并且把 TGC写入到 Cookie(login.xn.com域下),并且因为有 service的存在,(这里service主要是用来告诉CAS SERVER,你得告诉浏览器,一会儿重定向到源URL上才行),所以会生成 ST (service ticket),并告诉浏览器,重定向到 www.xn.com/1.html?ticket=st123456上.
    3. 浏览器会根据重定向的地址 www.xn.com/1.html?ticket=st123456上请求WEB服务器,所以,会继续进入 AuthenticationFilter ,但是这次因为有了ticket,根据源码,会进入下一个filter Cas20ProxyReceivingTicketValidationFilter, 该拦截器用来与CAS Server 进行身份核实,以确保 Service Ticket 的合法性.
    // 构造验证URL,向cas server发起验证请求 final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response)); if (log.isDebugEnabled()) { log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName()); } // 如果验证成功,设置assertion,当再一次发起访问请求时,如果发现assertion已经被设置,所以已经通过验证,不过再次重定向会cas认证中心 request.setAttribute(CONST_CAS_ASSERTION, assertion); if (this.useSession) { request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion); }
    这里会发现,如果从CAS SERVER上验证ST没问题,就应该是登录成功了,会设置 assertion到Session中. 这里写了,会构造URL,感觉应该类似于 http client针对 cas server发起了一次请求,然后返回 assertion.
    // 生成验证URL,如果你debug会发现,此处会构造一个类似以下的URL,访问的是cas server的serviceValidate方法 ,示例如下 // https://demo.testcas.com/cas/serviceValidate?ticket=ST-31-cioaDNxSpUWIgeYEn4yK-cas&service=http%3A%2F%2Fapp1.testcas.com%2Fb2c-haohai-server%2Fuser%2FcasLogin
    在 CAS SEVER那里, 会触发 final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
    这个函数, 所以,是感觉server与 ST的对应关系,来检查ST是否有效
    如果发现没问题了,就会通知浏览器,重定向 到 www.xn.com/1.html上,就可以了,这是最后一次重定向(这里的assertion感觉应该也会保存用户的一些信息,不然不可能每次client getuser的时候,都要链接CAS SERVER).
    3. 当浏览器收到重定向通知的时候, 就开始访问 源URL www.xn.com/1.html, 同样会触发 filter,这是,第一个filter会检查,发现已经登录了,然后交给ticketfilter, 发现没有 ticket,继续沿着 chain往下(剩下的应该不重要,会继续触发其他的HTTP行为了),这就是第一登录的完整过程了.
    当用户继续新标签打开www.xn.net的其他链接,都会被authenticationfilter拦截, session存在,并且有 assertion,所以,会是正常的.
    如果这个时候,关闭浏览器 jsession会失效,所以,再打开浏览器的时候,访问又有变化了
    因为session里没有 assertion了(jsession如果变了,肯定找不到用户上次回话对应的session了),所以这次访问,authentication会检查到session失效了,于是,又开始重定向到 https://login.xn.com/login?service=***上,但是 因为 login.xn.com的cookie的存在(里面有TGC),所以CAS SERVER会根据TGC知道,这个用户登录过,于是就可以不用登录了,直接生成 ST,然后告诉浏览器重定向到 service对应的RUL+?ticket=st333333,继续上面的验证了.
    (这里有一点,登录成功后 ,jsession id 跟 service 的对应关系,也会保留在CAS SERVER的,这个应该是注销的时候,会用到.)
    当用户访问 cs.xn.com/productCenter/index.html的时候,因为配置的CAS CLIENT,所以,会依然被重定向到 https://login.xn.com/login?service=cs.xn.com/productCenter/index.html ,这时,跟上面类同,也会先查TGC,发现登录过,直接返回 st,然后继续校验走一遍,就可以通过 assertion获取 user了,无须登录.
    这里项目出现过一个问题, 项目首页是个静态页面,里面有ajax异步请求JAVA的后台服务,所以,如果session莫名失效, ajax请求的时候,会被 filter重定向,因为跨域的关系,请求肯定会失败的.
    如果只是单单解决这个问题,可以随便在ajax执行前,有其他没有被排除的静态资源访问,先触发filter的话,应该可以避免ajax跨域重定向的问题.(比如,可以加载一个 不存在的img(这样不会被缓存,这个404的img会直接请求WEB服务的,或者在第一个 <script>里,var img = new img("src"),也会使AJAX确定不会先执行).
    登录过程是这个样子的, 登出过程没有详细的了解,只是简单说一下从网上看来的解释.
    比如 www.xn.com 有注销功能, 会去请求 https://login.xn.com/logout(后面有没有service我也不知道), CAS SERVER会检查 login.xn.com域里的 Cookie,里面有TGC,所以可以找到很多信息.
    CAS SERVER的代码
    1. //取得TGT_ID  
    2.      final String ticketGrantingTicketId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);  
    3. // 取得service参数数据,这个参数是可选参数  
    4.      final String service = request.getParameter("service");  
    5.        
    6.      //如果TGT不为空  
    7.      if (ticketGrantingTicketId != null) {  
    8.         //那么在centralAuthenticationService中销毁  
    9.          this.centralAuthenticationService  
    10.              .destroyTicketGrantingTicket(ticketGrantingTicketId);  
    11.          //ticketGrantingTicketCookieGenerator 中销毁cookie  
    12.          this.ticketGrantingTicketCookieGenerator.removeCookie(response);  
    13.          //warnCookieGenerator 中销毁  
    14.          this.warnCookieGenerator.removeCookie(response);  
    15.      }  
    16.      // 如果参数:followServiceRedirects为true 同时service不会空的时候,跳转到service指定的URL  
    17.      if (this.followServiceRedirects && service != null) {  
    18.          return new ModelAndView(new RedirectView(service));  
    19.      }  
    20.      //否则,跳转到logoutView指定的页面  
    21.      return new ModelAndView(this.logoutView);  
    1. public void destroyTicketGrantingTicket(final String ticketGrantingTicketId) {  
    2.         //断言参数不能空  
    3.         Assert.notNull(ticketGrantingTicketId);  
    4.           
    5.         if (log.isDebugEnabled()) {  
    6.             log.debug("Removing ticket [" + ticketGrantingTicketId + "] from registry.");  
    7.         }  
    8.         // 从票据仓库中取得TGT票据  
    9.         final TicketGrantingTicket ticket = (TicketGrantingTicket) this.ticketRegistry.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);  
    10.         //如果票据为空,则直接返回  
    11.         if (ticket == null) {  
    12.             return;  
    13.         }  
    14.   
    15.         if (log.isDebugEnabled()) {  
    16.             log.debug("Ticket found.  Expiring and then deleting.");  
    17.         }  
    18.         //叫票据注销,也就是设置为期满(或者叫做过期)  
    19.         ticket.expire();  
    20.         //在票据仓库中删除该票据  
    21.         this.ticketRegistry.deleteTicket(ticketGrantingTicketId);  
    22.     }  
    public synchronized void expire() {
            this.expired = true;
            logOutOfServices();
        }
    1. private void logOutOfServices() {  
    2.        for (final Entry<String, Service> entry : this.services.entrySet()) {  
    3.   
    4.            if (!entry.getValue().logOutOfService(entry.getKey())) {  
    5.                LOG.warn("Logout message not sent to [" + entry.getValue().getId() + "]; Continuing processing...");     
    6.            }  
    7.        }  
    8.    }  
    原来在TGT票据里面有个Entry来保存用户访问过的service对象,所以,这里的services的列表,会循环这个列表 给 CLIENT发送注销请求的. key是对应service的seesionID ,所以,这里每个用户的jsessionid,就有作用了.
    下面是发送请求的代码
    1. public synchronized boolean logOutOfService(final String sessionIdentifier) {  
    2.        if (this.loggedOutAlready) {  
    3.            return true;  
    4.        }  
    5.   
    6.        LOG.debug("Sending logout request for: " + getId());  
    7.   
    8.        final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID=""  
    9.            + GENERATOR.getNewTicketId("LR")  
    10.            + "" Version="2.0" IssueInstant="" + SamlUtils.getCurrentDateAndTime()  
    11.            + ""><saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID><samlp:SessionIndex>"  
    12.            + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";  
    13.          
    14.        this.loggedOutAlready = true;  
    15.          
    16.        if (this.httpClient != null) {  
    17.            return this.httpClient.sendMessageToEndPoint(getOriginalUrl(), logoutRequest, true);  
    18.        }  
    19.          
    20.        return false;  
    21.    }  
    客户端需要在 authencationfilter前加上一个 logout的filter和 listener
    <filter>
       <filter-name>CAS Single Sign Out Filter</filter-name>
       <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
    </filter>
    <filter-mapping>
       <filter-name>CAS Single Sign Out Filter</filter-name>
       <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
    </listener>
    SingleSignOutFilter:
    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain      
     2 
     3 filterChain) throws IOException, ServletException {
     4         final HttpServletRequest request = (HttpServletRequest) servletRequest;
     5 
     6         if ("POST".equals(request.getMethod())) {
     7             final String logoutRequest = request.getParameter("logoutRequest");
     8 
     9             if (CommonUtils.isNotBlank(logoutRequest)) {
    10 
    11                 if (log.isTraceEnabled()) {
    12                     log.trace ("Logout request=[" + logoutRequest + "]");
    13                 }
    14                 //从xml中解析 SessionIndex key值
    15                 final String sessionIdentifier = XmlUtils.getTextForElement(logoutRequest, "SessionIndex");
    16 
    17                 if (CommonUtils.isNotBlank(sessionIdentifier)) {
    18                         //根据sessionId取得session对象
    19                     final HttpSession session = SESSION_MAPPING_STORAGE.removeSessionByMappingId(sessionIdentifier);
    20 
    21                     if (session != null) {
    22                         String sessionID = session.getId();
    23 
    24                         if (log.isDebugEnabled()) {
    25                             log.debug ("Invalidating session [" + sessionID + "] for ST [" + sessionIdentifier + "]");
    26                         }
    27                         
    28                         try {
    29                 //让session失效
    30                             session.invalidate();
    31                         } catch (final IllegalStateException e) {
    32                             log.debug(e,e);
    33                         }
    34                     }
    35                   return;
    36                 }
    37             }
    38         } else {//get方式 表示登录,把session对象放到SESSION_MAPPING_STORAGE(map对象中)
    39             final String artifact = request.getParameter(this.artifactParameterName);
    40             final HttpSession session = request.getSession();
    41             
    42             if (log.isDebugEnabled() && session != null) {
    43                 log.debug("Storing session identifier for " + session.getId());
    44             }
    45             if (CommonUtils.isNotBlank(artifact)) {
    46                 SESSION_MAPPING_STORAGE.addSessionById(artifact, session);
    47             }
    48         }
    49 
    50         filterChain.doFilter(servletRequest, servletResponse);
    51     }
    先不管别的, 这么看,应该是 CAS SERVER 的 HTTPCLIENT 发起了一个类似
    http://cs.xn.com/***?logoutRequest*** 大概这样子的请求, 里面有 session id(因为 server是遍历的形式,所以,即使有些CLIENT这边失效的sessionid,也会发过来)然后从 SingleSignOutHttpSessionListener 持有的全局 session表中获得 session实例,然后 session.invalidate();失效.并且从 全局 SESSION_MAPPING_STORAGE 里移除.
    这样子,应该所有的子系统都会登出了.
  • 相关阅读:
    SpringBoot------异步任务的使用
    SpringBoot------定时任务
    MySQL中文编码设置为utf-8
    MySQL 中文未正常显示
    使用postman测试接口时需要先登录怎么办
    python 查询数据库返回的数据类型
    数据库和数据仓库的关系
    distinct 用法
    Hbase学习
    顺序访问数据和随机访问数据
  • 原文地址:https://www.cnblogs.com/davytitan/p/7904498.html
Copyright © 2020-2023  润新知