• CAS实现单点登录


    1.简介

     

    SSO单点登录

    在多个相互信任的系统中,用户只需要登录一次就可以访问其他受信任的系统。

     

     

    新浪微博与新浪博客是相互信任的应用系统。

    *当用户首次访问新浪微博时,新浪微博识别到用户未登录,将请求重定向到认证中心,认证中心也识别到用户未登录,则将请求重定向到登录页。

    *当用户已登录新浪微博访问新浪博客时,新浪博客识别到用户未登录,将请求重定向到认证中心,认证中心识别到用户已登录,返回用户的身份,此时用户无需登录即可使用新浪博客。

    *只要多个系统使用同一套单点登录框架那么它们将是相互信任的。

     

    CAS 

    Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法, CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。 

     

     

    CAS包含CAS ClientCAS Server两部分

    CAS Client:要使用单点登录的Web应用,将与同组下的Web应用构成相互信任的关系,只需在web应用中添加CAS提供的ListenerFilter即可成为CAS Client ,其主要负责对客户端的请求进行登录校验、重定向和校验ticket工作。

    CAS Server:主要负责对用户的用户名/密码进行认证,颁发票据等,需要单独的进行部署。

    *同组下的任意一个Web应用登录后其他应用都不需要登录即可使用。

     

     

    2.CAS服务器搭建

     

    2.1 去CAS官网下载CAS源码包

     

    将下载的源码包中的cas-server-webapp工程导入ide中,将工程打包为war包,直接放入tomcat下的webapp中运行。

    *CAS 5.0版本以上需要jdk1.8和gradle进行构建、4.X版本使用maven进行构建(maven 3.3+)

     

    2.2 在Tomcat中开启HTTPS协议

     

    *由于CAS Server默认使用HTTPS协议进行访问,因此需要在Tomcat中开启HTTPS协议。

     

    1.使用JDK提供的keytool命令生成秘钥库。

     

    2.修改tomcat配置并开启8443端口

     

    在tomcat/conf/server.xml中添加:

    <!-- 单向认证 -->
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
        maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
       clientAuth="false" sslProtocol="TLS" keystoreFile="www.gimc.cn.keystore" keystorePass="123456"  />

     

    校验Tomcat是否支持HTTPS协议: https://localhost:8443/

     

     

    2.3 进入CAS认证中心

     

    登录处理地址:https://localhost:8443/cas-server-webapp-4.2.7/login

     

     

    *由于首次访问,客户端浏览器进程所占用的内存中不存在TGC Cookie,所以CAS Server认为用户未进行登录,因此将请求转发到登录页面。

    *默认账号:casuser/Mellon

     

     

     

    *当登录后再次访问登录处理时,将会直接转发到已登录页面。

    *CAS Server根据Cookie (TGC是否能够匹配TGT)来判断用户是否已进行登录,默认情况下TGC Cookie位于浏览器进程所占用的内存中,因此当关闭浏览器时Cookie失效(TGC失效),此时再访问CAS登录处理时将需要重新进行登录,当CAS服务器重启时,TGT将会失效(位于服务器内存),此时也需要重新进行登录。

    *当用户登录后,CAS Server会维护TGT与用户身份信息的关系,所有CAS Client可以从CAS Server中获取当前登录的用户的身份信息。 

     

    注销处理地址:https://localhost:8443/cas-server-webapp-4.2.7/logout

     

     

    *在已登录的状态下访问注销地址将会提示注销成功,其经过以下步骤:

    1.清除保存在客户端浏览器进程所占用的内存中的TGC Cookie(设空)

    2.清除保存在服务器的TGT。

    3.通过HTTP请求分别通知当前用户所有已登录的CAS Client进行注销登录操作,销毁用户对应的Session对象。

    *当注销成功后,此时再访问登录页面时需重新登录。

     

     

    2.4 修改为自定义数据源

     

    1.修改cas-server-webapp/WEB-INF/deployerConfigContext.xml

     

    注释配置:

    <!-- <alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" /> -->

     

    新增配置:

      <!-- 对密码进行加密 -->
      <bean id="passwordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
        <constructor-arg value="MD5"></constructor-arg>
        <property name="characterEncoding" value="UTF-8"></property>
      </bean>
    
      <!-- 自定义数据源 -->
      <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/cas?useUnicode=true&amp;characterEncoding=UTF-8"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
      </bean>
    
      <!-- 认证控制器 -->
      <bean id="queryDatabaseAuthenticationHandler" name="primaryAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
        <property name="passwordEncoder" ref="passwordEncoder" />
        <property name="dataSource" ref="dataSource" />
        <!-- 通过用户名查询密码的SQL --> 
        <property name="sql" value="select password from sys_user where username =?" />
      </bean>

     

    2.在cas-server-webapp/WEB-INF/lib包中添加:cas-server-support-jdbc.jar、mysql-connector-java.jar

     

     

    2.5 修改为HTTP方式访问

     

    1.修改cas-server-webapp/WEB-INF/cas.properties

    tgc.secure=false
    warn.cookie.secure=false

     

    2.修改cas-server-webapp/WEB-INF/classes/services/HTTPSandIMAPS-10000001.json

    "serviceId" : "^(https|imaps|http)://.*"

    *修改serviceId的值即可。

     

    3.删除cas-server-webapp/WEB-INF/view/jsp/default/ui/casLoginView.jsp页面中校验是否是HTTPS协议的标签块。

    <c:if test="${not pageContext.request.secure}">
        <div id="msg" class="errors">
            <h2><spring:message code="screen.nonsecure.title" /></h2>
            <p><spring:message code="screen.nonsecure.message" /></p>
        </div>
    </c:if>

     

     

    3.CAS客户端搭建

     

    3.1 引入Maven依赖

     

    <dependency>
          <groupId>org.jasig.cas.client</groupId>
          <artifactId>cas-client-core</artifactId>
          <version>3.2.0</version>
    </dependency>

     

     

    3.2 在web.xml中配置CAS提供的Listener、Filter

     

    <!-- 单点退出Listener -->
    <listener>
        <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
    </listener>
    
    <!-- 单点退出Filter -->
    <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>
    
    <!-- CAS认证Filter -->
    <filter>
        <filter-name>CASFilter</filter-name>
        <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
            <init-param>
                <!-- CAS登录页面,当SessionId无法匹配Session时,跳转到CAS登录页面 -->
                <param-name>casServerLoginUrl</param-name>
                <param-value>http://localhost:8080/cas-server-webapp-4.2.7/login</param-value>
            </init-param>
            <init-param>
                <param-name>serverName</param-name>
                <param-value>http://localhost:8080</param-value>
            </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CASFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <!-- CAS Ticket校验Filter -->
    <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>
                <param-value>http://localhost:8080/cas-server-webapp-4.2.7</param-value>
            </init-param>
            <init-param>
                <param-name>serverName</param-name>
                <param-value>http://localhost:8080</param-value>
            </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CAS Validation Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <!-- 使客户端支持通过AssertionHolder来获取用户的登录名 -->  
    <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>  

     

    *各个客户端可通过AssertionHolder.getAssertion().getPrincipal().getName()获取当前登录用户的用户名。

     

     

    4.CAS原理分析

     

     

    4.1 项目架构图

     

     

    4.2 用户第一次访问项目A

     

    http://localhost:8080/A/testCas

     

    1.请求将到达项目A的CAS认证Filter。

    2.CAS认证Filter判断是否能通过SessionId Cookie匹配到Session对象,并且Session对象中是否存在name为_const_cas_assertion_的属性(该属性中存放着Assertion实体)

    3.若存在Assertion实体,则放行,将请求交给下一个过滤器进行处理( ticket检验filter ),若不存在Assertion实体,则构造Service参数,并且判断请求中是否携带了ticket参数。

    4.若存在ticket参数,则放行,将请求交给下一个过滤器进行处理( ticket检验filter ),若不存在ticket参数,则将请求重定向到CAS Server登录处理。

     

    CAS认证Filter

        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;
            //当SessionId Cookie无法匹配Session时返回null,并不会创建新的Session对象.
            final HttpSession session = request.getSession(false);
            //判断Session中是否存在name为_const_cas_assertion_的属性,存在则返回Assertion实体,否则返回Null.
            final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
            
            //存在Assertion实体则直接放行,将请求交给下一个过滤器处理.
            if (assertion != null) {
                filterChain.doFilter(request, response);
                return;
            }
    
            //构造ServiceUrl用于封装在service参数中.
            final String serviceUrl = constructServiceUrl(request, response);
            //判断请求中是否存在ticket参数,若存在则说明是CAS Server的回调请求.
            final String ticket = CommonUtils.safeGetParameter(request,getArtifactParameterName());
            final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
            //存在ticket参数则直接放行,将请求交给下一个过滤器处理,否则将请求重定向到CAS Server登录处理,并在请求URL后追加service参数传递原访问的URL.
            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);
            }
    
            final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
    
            if (log.isDebugEnabled()) {
                log.debug("redirecting to "" + urlToRedirectTo + """);
            }
    
            response.sendRedirect(urlToRedirectTo);
        }

     

    *由于用户第一次访问项目A,并没有携带SessionId Cookie,因此无法成功匹配Session,所以Assertion实体为null,请求中也不存在ticket参数,则此时项目A认为该用户未登录,返回302状态码示意浏览器将请求重定向到CAS Server进行登录处理,并在请求URL后追加service参数传递原访问项目A的URL。

    *CAS Client根据Session中是否存在Assertion属性判断当前用户是否已登录。

     

     

    *当浏览器收到项目A返回的302重定向请求后,对重定向目标地址重新发起HTTP请求,最终到达CAS Server进行登录处理,由于浏览器不存在TGC Cookie,CAS Server认为用户未进行登录,因此将请求转发到登录页面。 

     

     

    *输入用户名/密码进行提交

     

    *CAS Server对用户输入的用户名/密码进行校验,若校验成功则返回302状态码示意浏览器将请求重定向到原访问项目A的URL地址并在URL后追加ticket参数传递ST,并且最终保存TGC Cookie在客户端浏览器进程所占用的内存中。

    TGC:Ticket Granted Cookie , 以Cookie的形式保存在客户端浏览器所占用的内存中(Cookie值)

    TGT:Ticket Granted Ticket,保存在CAS服务器的内存中,其可以签发ST。

    ST:Service Ticket,由TGT签发,最终通过URL传给CAS Client。

    *CAS Server根据TGC匹配TGT,TGT又与用户的身份信息相关联。

    *当用户登录成功后,此时客户端就存在TGC Cookie,CAS服务端就存在对应的TGT。

     

     

     

    *当浏览器收到CAS Server返回的302重定向请求后,对重定向目标地址重新发起HTTP请求( 携带ticket参数 ),此时请求将会首先进入项目A的CAS认证Filter,由于当前不存在SeesionId Cookie,不存在Session对象包含name为_const_cas_assertion_的属性,但由于请求中包含了ticket参数,此时就会放行,将请求交给下一个过滤器处理。

     

    5.请求将进入CAS Ticket验证Filter。

    6.判断请求中是否存在ticket参数,若存在则进入Ticket校验流程,否则直接放行,将请求交给下一个过滤器或直接到达目标资源。

    7.若存在ticket,则通过HTTP的方式访问CAS Server进行ticket的合法性校验,若校验成功则生成Session对象并且将Assertion实体放入Session中,最终将请求重定向原访问项目的地址,若校验失败则返回403状态码,标识无权限访问资源。

     

    CAS Ticket校验Filter

       public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
    
            if (!preFilter(servletRequest, servletResponse, filterChain)) {
                return;
            }
    
            final HttpServletRequest request = (HttpServletRequest) servletRequest;
            final HttpServletResponse response = (HttpServletResponse) servletResponse;
            final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());
            //如果HttpServletRequest中包含ticket参数则进行ticket的合法性校验,否则直接放行.
            if (CommonUtils.isNotBlank(ticket)) {
                if (log.isDebugEnabled()) {
                    log.debug("Attempting to validate ticket: " + ticket);
                }
    
                try {
                    //通过HTTP访问CAS Server进行ticket的合法性校验
                    final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));
    
                    if (log.isDebugEnabled()) {
                        log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName());
                    }
    
                    request.setAttribute(CONST_CAS_ASSERTION, assertion);
    
                    if (this.useSession) {
                        //当ticket校验成功则将Assertion实体放入Session中
                        request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
                    }
                    onSuccessfulValidation(request, response, assertion);
    
                    if (this.redirectAfterValidation) {
                        log. debug("Redirecting after successful ticket validation.");
                        //将请求重定向到原访问的URL
                        response.sendRedirect(constructServiceUrl(request, response));
                        return;
                    }
                } catch (final TicketValidationException e) {
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    log.warn(e, e);
    
                    onFailedValidation(request, response);
    
                    if (this.exceptionOnValidationFailure) {
                        throw new ServletException(e);
                    }
    
                    return;
                }
            }
    
            filterChain.doFilter(request, response);
    
        }

     

     

     

     

    *当浏览器收到项目A返回的302重定向请求后,重新请求最初访问项目A的URL地址。

    *由于携带了SessionId Cookie并且能成功匹配Session对象,由于已登录过,Session中存在name为_const_cas_assertion_的属性,因此允许访问资源。

     

     

    4.3 用户再次访问项目A

     

     

    *由于携带了SessionId Cookie并且能成功匹配Session对象,由于已登录过,Session中存在name为_const_cas_assertion_的属性,因此允许访问资源。

     

     

    4.4 用户第一次访问项目B

     

    http://localhost:8080/B/testCas

     

     

    *用户第一次访问项目B,并没有携带SessionId Cookie,因此无法成功匹配Session,所以Assertion实体为null,请求中也不存在ticket参数,此时项目B认为该用户未登录,返回302状态码示意浏览器将请求重定向到CAS Server进行登录处理,并在请求URL后追加service参数传递原访问项目B的URL。

     

     

    *当浏览器收到项目B返回的302重定向请求后,对重定向目标地址重新发起HTTP请求,最终到达CAS Server进行登录处理,由于客户端浏览器中存在TGC Cookie,并且CAS Server成功根据TGC匹配TGT,所以CAS Server认为该用户已经进行登录,最终通过TGT签发ST,返回302状态码示意浏览器将请求重定向到原访问项目B的URL,并在URL追加ticket参数传递ST。

     

     

    *当浏览器收到CAS Server返回的302重定向请求后,对重定向目标地址重新发起HTTP请求( 携带ticket参数 ),此时请求将会进入项目B的ticket认证Filter中,项目B将对ticket进行有效性校验( 内部访问Cas Server进行校验 ),若校验成功则生成Session对象并将Assertion实体放入Session中,最终将请求重定向到原访问项目B的地址。

     

     

     

    *当浏览器收到项目B返回的302重定向请求后,重新请求最初访问项目B的URL地址。

    *由于携带了SessionId Cookie并且能成功匹配Session对象,由于已登录过,Session中存在name为_const_cas_assertion_的属性,因此允许访问资源。

     

     

    4.5 注销

     

    访问CAS Server注销处理地址:http://localhost:8080/cas-server-webapp-4.2.7/logout

     

     

    *当访问CAS注销地址后:

    1.清除位于客户端浏览器进程所占用的内存中的TGC Cookie (设空)

    2.清除位于CAS Server中对应的TGT。

    3.通过HTTP请求分别通知当前用户所有已登录的CAS Client进行注销登录操作,此时请求将会进入CAS Client的单点登出Filter,单点登出Filter中判断当前请求是否是POST请求方式并且是否携带了logoutRequest参数,若不属于则放行,将请求交给下一个过滤器进行处理,若属于则进行Session对象的销毁。

     

    *当注销后,TGC、TGT、CAS Client用户对应的Session对象将会失效,此时再访问项目A和项目B需要重新登录。

     

    CAS单点登出Filter

        public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
            final HttpServletRequest request = (HttpServletRequest) servletRequest;
            //判断请求参数是否携带ticket参数,即CAS Server的回调URL,用于Session的记录操作.
            if (handler.isTokenRequest(request)) {
                handler.recordSession(request);
            //判断请求参数是否携带logoutRequest参数,即CAS Server注销时通知CAS Client的URL,用于Session的销毁.
            } else if (handler.isLogoutRequest(request)) {
                handler.destroySession(request);
                // Do not continue up filter chain
                return;
            } else {
                log.trace("Ignoring URI " + request.getRequestURI());
            }
    
            filterChain.doFilter(servletRequest, servletResponse);
        }

     

        public void destroySession(final HttpServletRequest request) {
            //获取HTTP请求中的logoutRequest参数
            final String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName);
            if (log.isTraceEnabled()) {
                log.trace ("Logout request:
    " + logoutMessage);
            }
            //解析XML获取ticket值
            final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
            //如果ticket值不为空则执行Session的invalidate()方法销毁Session对象.
            if (CommonUtils.isNotBlank(token)) {
                final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
    
                if (session != null) {
                    String sessionID = session.getId();
    
                    if (log.isDebugEnabled()) {
                        log.debug ("Invalidating session [" + sessionID + "] for token [" + token + "]");
                    }
                    try {
                        session.invalidate();
                    } catch (final IllegalStateException e) {
                        log.debug("Error invalidating session.", e);
                    }
                }
            }
        }

     

    *logoutRequest参数的值是XML:

    <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-1-1zjgcguvShbJrsNLbbfQ5Rk5LbfHblgGHep" Version="2.0" IssueInstant="2018-07-23T16:46:32Z">
      <saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID>
      <samlp:SessionIndex>ST-1-2EiBwiJuD5vbhYghmMS5-cas01.example.org</samlp:SessionIndex>
    </samlp:LogoutRequest>

     

     

    4.6 关闭浏览器

     

    *当关闭浏览器后Cookie失效(SessionId、TGC失效),此时再访问项目A和项目B时将需要重新登录。

     

     

    4.7 重启CAS Server

     

    *当CAS Server重启后,TGT将会失效(位于服务器内存),TGC无法成功匹配TGT,但此时访问项目A和项目B时不需要重新登录,因为其Session对象中仍存在Assertion实体。

    *当CAS Client重启后,无须再登录也可以使用。

     

  • 相关阅读:
    《Java大学教程》—第12章 案例研究--第2部分
    《Java大学教程》—第11章 案例研究--第1部分
    《Java大学教程》—第10章 图形和事件驱动程序
    《Java大学教程》—第8章 通过继承扩展类
    《Java大学教程》—第7章 类的实现
    《Java大学教程》—第6章 类和对象
    《Java大学教程》—第5章 数组
    《Java大学教程》—第4章 方法的实现
    spring_01概念及案例
    MyEclipse中jsp编码设置
  • 原文地址:https://www.cnblogs.com/funyoung/p/9234947.html
Copyright © 2020-2023  润新知