在多个相互信任的系统中,用户只须要登陆一次就能够访问其余受信任的系统。java
新浪微博与新浪博客是相互信任的应用系统。mysql
*当用户首次访问新浪微博时,新浪微博识别到用户未登陆,将请求重定向到认证中心,认证中心也识别到用户未登陆,则将请求重定向到登陆页。git
*当用户已登陆新浪微博访问新浪博客时,新浪博客识别到用户未登陆,将请求重定向到认证中心,认证中心识别到用户已登陆,返回用户的身份,此时用户无需登陆便可使用新浪博客。github
*只要多个系统使用同一套单点登陆框架那么它们将是相互信任的。web
Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登陆方法, CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。 spring
CAS包含CAS Client 和 CAS Server两部分sql
CAS Client:要使用单点登陆的Web应用,将与同组下的Web应用构成相互信任的关系,只需在web应用中添加CAS提供的Listener和Filter便可成为CAS Client ,其主要负责对客户端的请求进行登陆校验、重定向和校验ticket工做。apache
CAS Server:主要负责对用户的用户名/密码进行认证,颁发票据等,须要单独的进行部署。json
*同组下的任意一个Web应用登陆后其余应用都不须要登陆便可使用。浏览器
将下载的源码包中的cas-server-webapp工程导入ide中,将工程打包为war包,直接放入tomcat下的webapp中运行。
*CAS 5.0版本以上须要jdk1.8和gradle进行构建、4.X版本使用maven进行构建(maven 3.3+)
*因为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/
登陆处理地址: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对象。
*当注销成功后,此时再访问登陆页面时需从新登陆。
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&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
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>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.2.0</version>
</dependency>
<!-- 单点退出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()获取当前登陆用户的用户名。
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_的属性,所以容许访问资源。
*因为携带了SessionId Cookie而且能成功匹配Session对象,因为已登陆过,Session中存在name为_const_cas_assertion_的属性,所以容许访问资源。
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_的属性,所以容许访问资源。
访问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:\n" + 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>
*当关闭浏览器后Cookie失效(SessionId、TGC失效),此时再访问项目A和项目B时将须要从新登陆。
*当CAS Server重启后,TGT将会失效(位于服务器内存),TGC没法成功匹配TGT,但此时访问项目A和项目B时不须要从新登陆,由于其Session对象中仍存在Assertion实体。
*当CAS Client重启后,无须再登陆也能够使用。