關於CAS很多的原理和基礎的配置啓動,網上是很多的,我更多是結合我的實踐和心得。需要了解CAS的原理,認證協議,認證流程,可以參考以下文章。
背景
單點登錄(SSO)是企業開發的重要問題,在我的畢設項目中,由於要和系統其他開發模塊共用用戶認證模塊,方便共享用戶資源,所以需要一個好的SSO解決方案。
一般SSO的實現機制有兩種:基於session的和基於cookie的。WebLogic通過Session共享認證信息。Session是一種服務器端機制,當客戶端訪問服務器時,服務器爲客戶端創建一個惟一的SessionID,以使在整個交互過程中始終保持狀態,而交互的信息則可由應用自行指定,因此用Session方式實現SSO,不能在多個瀏覽器之間實現單點登錄,但卻可以跨域;WebSphere通過Cookie記錄認證信息。Cookie是一種客戶端機制,它存儲的內容主要包括: 名字、值、過期時間、路徑和域,路徑與域合在一起就構成了Cookie的作用範圍,因此用Cookie方式可實現SSO,但域名必須相同。對應這兩者,開源的SSO實現分別是OAuth和CAS。
OAuth更多的是解決第三方去訪問服務提供方的用戶的資源,我認爲更適用於不同的系統,比如大的平臺都會提供OAuth的認證機制(新浪微博,google)。而CAS更貼近我的需求,就是解決同一系統下不同服務間的用戶認證工作,可以無縫連接。
關於CAS
- <?xml version="1.0" encoding="UTF-8"?>
- <!--
- | deployerConfigContext.xml centralizes into one file some of the declarative configuration that
- | all CAS deployers will need to modify.
- |
- | This file declares some of the Spring-managed JavaBeans that make up a CAS deployment.
- | The beans declared in this file are instantiated at context initialization time by the Spring
- | ContextLoaderListener declared in web.xml. It finds this file because this
- | file is among those declared in the context parameter "contextConfigLocation".
- |
- | By far the most common change you will need to make in this file is to change the last bean
- | declaration to replace the default SimpleTestUsernamePasswordAuthenticationHandler with
- | one implementing your approach for authenticating usernames and passwords.
- +-->
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:p="http://www.springframework.org/schema/p"
- xmlns:sec="http://www.springframework.org/schema/security"
- xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
- http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">
- <!--
- | This bean declares our AuthenticationManager. The CentralAuthenticationService service bean
- | declared in applicationContext.xml picks up this AuthenticationManager by reference to its id,
- | "authenticationManager". Most deployers will be able to use the default AuthenticationManager
- | implementation and so do not need to change the class of this bean. We include the whole
- | AuthenticationManager here in the userConfigContext.xml so that you can see the things you will
- | need to change in context.
- +-->
- <bean id="authenticationManager"
- class="org.jasig.cas.authentication.AuthenticationManagerImpl">
- <!--
- | This is the List of CredentialToPrincipalResolvers that identify what Principal is trying to authenticate.
- | The AuthenticationManagerImpl considers them in order, finding a CredentialToPrincipalResolver which
- | supports the presented credentials.
- |
- | AuthenticationManagerImpl uses these resolvers for two purposes. First, it uses them to identify the Principal
- | attempting to authenticate to CAS /login . In the default configuration, it is the DefaultCredentialsToPrincipalResolver
- | that fills this role. If you are using some other kind of credentials than UsernamePasswordCredentials, you will need to replace
- | DefaultCredentialsToPrincipalResolver with a CredentialsToPrincipalResolver that supports the credentials you are
- | using.
- |
- | Second, AuthenticationManagerImpl uses these resolvers to identify a service requesting a proxy granting ticket.
- | In the default configuration, it is the HttpBasedServiceCredentialsToPrincipalResolver that serves this purpose.
- | You will need to change this list if you are identifying services by something more or other than their callback URL.
- +-->
- <property name="credentialsToPrincipalResolvers">
- <list>
- <!--
- | UsernamePasswordCredentialsToPrincipalResolver supports the UsernamePasswordCredentials that we use for /login
- | by default and produces SimplePrincipal instances conveying the username from the credentials.
- |
- | If you've changed your LoginFormAction to use credentials other than UsernamePasswordCredentials then you will also
- | need to change this bean declaration (or add additional declarations) to declare a CredentialsToPrincipalResolver that supports the
- | Credentials you are using.
- +-->
- <bean
- class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver" >
- <property name="attributeRepository" ref="attributeRepository" />
- </bean>
- <!--
- | HttpBasedServiceCredentialsToPrincipalResolver supports HttpBasedCredentials. It supports the CAS 2.0 approach of
- | authenticating services by SSL callback, extracting the callback URL from the Credentials and representing it as a
- | SimpleService identified by that callback URL.
- |
- | If you are representing services by something more or other than an HTTPS URL whereat they are able to
- | receive a proxy callback, you will need to change this bean declaration (or add additional declarations).
- +-->
- <bean
- class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" />
- </list>
- </property>
- <!--
- | Whereas CredentialsToPrincipalResolvers identify who it is some Credentials might authenticate,
- | AuthenticationHandlers actually authenticate credentials. Here we declare the AuthenticationHandlers that
- | authenticate the Principals that the CredentialsToPrincipalResolvers identified. CAS will try these handlers in turn
- | until it finds one that both supports the Credentials presented and succeeds in authenticating.
- +-->
- <property name="authenticationHandlers">
- <list>
- <!--
- | This is the authentication handler that authenticates services by means of callback via SSL, thereby validating
- | a server side SSL certificate.
- +-->
- <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
- p:httpClient-ref="httpClient" p:requireSecure="false" />
- <!--
- | This is the authentication handler declaration that every CAS deployer will need to change before deploying CAS
- | into production. The default SimpleTestUsernamePasswordAuthenticationHandler authenticates UsernamePasswordCredentials
- | where the username equals the password. You will need to replace this with an AuthenticationHandler that implements your
- | local authentication strategy. You might accomplish this by coding a new such handler and declaring
- | edu.someschool.its.cas.MySpecialHandler here, or you might use one of the handlers provided in the adaptors modules.
- +-->
- <!--
- <bean
- class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />
- -->
- <bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
- <property name="dataSource" ref="dataSource"></property>
- <property name="sql" value="select password from academic_user where username=?"></property>
- <!--
- <property name="passwordEncoder" ref="MD5PasswordEncoder"></property>
- -->
- </bean>
- </list>
- </property>
- </bean>
- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
- <property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
- <property name="url"><value>jdbc:mysql://localhost:3307/academic</value></property>
- <property name="username"><value>root</value></property>
- <property name="password"><value></value></property>
- </bean>
- <!--
- <bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
- <constructor-arg index="0">
- <value>MD5</value>
- </constructor-arg>
- </bean>
- -->
- <!--
- This bean defines the security roles for the Services Management application. Simple deployments can use the in-memory version.
- More robust deployments will want to use another option, such as the Jdbc version.
- The name of this should remain "userDetailsService" in order for Spring Security to find it.
- -->
- <!-- <sec:user name="@@THIS SHOULD BE REPLACED@@" password="notused" authorities="ROLE_ADMIN" />-->
- <sec:user-service id="userDetailsService">
- <sec:user name="@@THIS SHOULD BE REPLACED@@" password="notused" authorities="ROLE_ADMIN" />
- </sec:user-service>
- <!--
- Bean that defines the attributes that a service may return. This example uses the Stub/Mock version. A real implementation
- may go against a database or LDAP server. The id should remain "attributeRepository" though.
- -->
- <bean id="attributeRepository"
- class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao">
- <constructor-arg index="0" ref="dataSource"/>
- <constructor-arg index="1" value="select * from academic_user where {0}"/>
- <property name="queryAttributeMapping">
- <map>
- <entry key="username" value="username" />
- </map>
- </property>
- <property name="resultAttributeMapping">
- <map>
- <entry key="username" value="username"/>
- <entry key="name" value="name"/>
- <entry key="email" value="email"/>
- </map>
- </property>
- </bean>
- <!--
- Sample, in-memory data store for the ServiceRegistry. A real implementation
- would probably want to replace this with the JPA-backed ServiceRegistry DAO
- The name of this bean should remain "serviceRegistryDao".
- -->
- <bean
- id="serviceRegistryDao"
- class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
- <!--
- <property name="registeredServices">
- <list>
- <bean class="org.jasig.cas.services.RegisteredServiceImpl">
- <property name="id" value="0" />
- <property name="name" value="HTTP" />
- <property name="description" value="Only Allows HTTP Urls" />
- <property name="serviceId" value="http://**" />
- </bean>
- <bean class="org.jasig.cas.services.RegisteredServiceImpl">
- <property name="id" value="1" />
- <property name="name" value="HTTPS" />
- <property name="description" value="Only Allows HTTPS Urls" />
- <property name="serviceId" value="https://**" />
- </bean>
- <bean class="org.jasig.cas.services.RegisteredServiceImpl">
- <property name="id" value="2" />
- <property name="name" value="IMAPS" />
- <property name="description" value="Only Allows HTTPS Urls" />
- <property name="serviceId" value="imaps://**" />
- </bean>
- <bean class="org.jasig.cas.services.RegisteredServiceImpl">
- <property name="id" value="3" />
- <property name="name" value="IMAP" />
- <property name="description" value="Only Allows IMAP Urls" />
- <property name="serviceId" value="imap://**" />
- </bean>
- </list>
- </property>
- -->
- </bean>
- <bean id="auditTrailManager" class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager" />
- </beans>
注意上面一些我註釋掉的地方和添加的地方,我就不一一指出了,有什麼問題可以私下再問我。
在客戶端使用cas的時候,需要把cas-client的包導入web project/WEB-INF/lib裏,需要什麼包就用maven去打包特定的包。最關鍵的是web.xml文件裏對於filter的一些設定。在這些設定裏包括了cas的login和logout這倆最基礎的功能,還有一個很重要的是cas的validation。如果validation成功,cas會在session裏返回用戶名,而我在上面的xml裏還加入了別的用戶信息,這些東西會在validation成功之後寫入session裏,以xml的形式放着,我們可以用自己寫的AutoSetUserAdapterFilter來得到。下面是web.xml的配置,
- <?xml version="1.0" encoding="UTF-8"?>
- <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
- id="WebApp_ID" version="2.5">
- <display-name>AcademicSearchEngine</display-name>
- <welcome-file-list>
- <welcome-file>home.jsp</welcome-file>
- </welcome-file-list>
- <filter>
- <filter-name>struts2</filter-name>
- <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
- </filter>
- <filter-mapping>
- <filter-name>struts2</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
- <!-- 用於單點退出,該過濾器用於實現單點登出功能,可選配置 -->
- <listener>
- <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
- </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>/share.jsp</url-pattern>
- </filter-mapping>
- <!-- 該過濾器負責用戶的認證工作,必須啓用它 -->
- <filter>
- <filter-name>CAS Authentication Filter</filter-name>
- <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
- <init-param>
- <param-name>casServerLoginUrl</param-name>
- <param-value>http://dcd.academic:8443/cas/login</param-value>
- </init-param>
- <init-param>
- <!--這裏的server是服務端的IP -->
- <param-name>serverName</param-name>
- <param-value>http://dcd.academic:8080</param-value>
- </init-param>
- <init-param>
- <param-name>renew</param-name>
- <param-value>false</param-value>
- </init-param>
- <init-param>
- <param-name>gateway</param-name>
- <param-value>false</param-value>
- </init-param>
- </filter>
- <filter-mapping>
- <filter-name>CAS Authentication Filter</filter-name>
- <url-pattern>/share.jsp</url-pattern>
- </filter-mapping>
- <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://dcd.academic:8443/cas</param-value>
- </init-param>
- <init-param>
- <param-name>serverName</param-name>
- <param-value>http://dcd.academic:8080</param-value>
- </init-param>
- <init-param>
- <param-name>useSession</param-name>
- <param-value>true</param-value>
- </init-param>
- <init-param>
- <param-name>redirectAfterValidation</param-name>
- <param-value>true</param-value>
- </init-param>
- </filter>
- <filter-mapping>
- <filter-name>CAS Validation Filter</filter-name>
- <url-pattern>/share.jsp</url-pattern>
- </filter-mapping>
- <!-- 該過濾器負責實現HttpServletRequest請求的包裹, 比如允許開發者通過HttpServletRequest 的 getRemoteUser()方法獲得SSO登錄用戶的登錄名,可選配置。 -->
- <filter>
- <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
- <filter-class>
- org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
- </filter>
- <filter-mapping>
- <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
- <url-pattern>/share.jsp</url-pattern>
- </filter-mapping>
- <!-- 該過濾器使得開發者可以通過org.jasig.cas.client.util.AssertionHolder來獲取用戶的登錄名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 -->
- <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>/share.jsp</url-pattern>
- </filter-mapping>
- <!-- 自動根據單點登錄的結果設置本系統的用戶信息 -->
- <filter>
- <display-name>AutoSetUserAdapterFilter</display-name>
- <filter-name>AutoSetUserAdapterFilter</filter-name>
- <filter-class>dcd.academic.cas.AutoSetUserAdapterFilter</filter-class>
- </filter>
- <filter-mapping>
- <filter-name>AutoSetUserAdapterFilter</filter-name>
- <url-pattern>/share.jsp</url-pattern>
- </filter-mapping>
- </web-app>
- package dcd.academic.cas;
- import java.io.IOException;
- import java.util.Map;
- import javax.servlet.Filter;
- import javax.servlet.FilterChain;
- import javax.servlet.FilterConfig;
- import javax.servlet.ServletException;
- import javax.servlet.ServletRequest;
- import javax.servlet.ServletResponse;
- import javax.servlet.http.HttpServletRequest;
- import org.jasig.cas.client.util.AssertionHolder;
- import org.jasig.cas.client.validation.Assertion;
- import dcd.academic.DAO.DAOfactory;
- import dcd.academic.DAO.UserDAO;
- import dcd.academic.model.User;
- import dcd.academic.util.StdOutUtil;
- /**
- * CAS單點登陸的過濾器功能類,該類用來自動生成子應用的登陸Session
- *
- */
- public class AutoSetUserAdapterFilter implements Filter {
- /**
- * Default constructor.
- */
- public AutoSetUserAdapterFilter() {
- StdOutUtil.out("[AutoSetUserAdapterFilter]");
- }
- /**
- * @see Filter#destroy()
- */
- public void destroy() {
- }
- public void doFilter(ServletRequest request, ServletResponse response,
- FilterChain chain) throws IOException, ServletException {
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- // _const_cas_assertion_是CAS中存放登錄用戶名的session標誌
- Object object = httpRequest.getSession().getAttribute(
- "_const_cas_assertion_");
- if (object != null) {
- Assertion assertion = (Assertion) object;
- String loginName = assertion.getPrincipal().getName();
- StdOutUtil.out("[loginname]: " + loginName);
- Map<String, Object> map = assertion.getPrincipal().getAttributes();
- String email = (String) map.get("email");
- String name = (String) map.get("name");
- String username = (String) map.get("username");
- StdOutUtil.out("[email]: " + email);
- StdOutUtil.out("[name]: " + name);
- StdOutUtil.out("[username]: " + username);
- }
- chain.doFilter(request, response);
- }
- /**
- * @see Filter#init(FilterConfig)
- */
- public void init(FilterConfig fConfig) throws ServletException {
- }
- }
還有一點,就是在validation success的返回jsp裏,要新添加一些內容,在目錄cas\WEB-INF\view\jsp\protocol\2.0的casServiceValidationSuccess.jsp
- <%@ page session="false" %><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
- <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
- <cas:authenticationSuccess>
- <cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>
- <c:if test="${not empty pgtIou}">
- <cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
- </c:if>
- <c:if test="${fn:length(assertion.chainedAuthentications) > 1}">
- <cas:proxies>
- <c:forEach var="proxy" items="${assertion.chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(assertion.chainedAuthentications)-2}" step="1">
- <cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
- </c:forEach>
- </cas:proxies>
- </c:if>
- <c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}">
- <cas:attributes>
- <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}">
- <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
- </c:forEach>
- </cas:attributes>
- </c:if>
- </cas:authenticationSuccess>
- </cas:serviceResponse>
其實本質上這些都是servlet的處理。因爲cas也是一個servlet寫成的war,說簡單也簡單。所以cas自己的登錄界面我們都是自己自己定製的。
我們在使用的時候,需要改動的項目代碼很少。在需要登錄或者認證的地方,把鏈接跳轉到server:8443/cas/login上,登錄成功後讓cas的登錄成功界面跳轉回原service的url即可,這時候cas是通過service和service ticket生成了新的ticket grant ticket,然後在session裏存了東西讓客戶端去讀取的。在安全方面,這步是在SSL的基礎上做的,所以我直接訪問如server:8443/cas/serviceValidation是會出SSL證書錯誤的。
還是稍微說一下cas的協議機制吧。這張圖也是別人文章裏的圖,爲了方便大家理解,還是帖一下。
總結
總結cas的話,我們可以單獨給一個tomcat來做用戶認證模塊,並且認證之後,客戶端是可以得到session裏的用戶信息的。可以認爲這樣就把單點登錄問題解決了。至於這個cas服務器怎麼配置,怎麼認證,需要傳遞什麼的,就去tomcat/webapps/cas的許許多多jsp和xml裏去配置。話說這些jsp和xml真的很多。
像這樣的開源企業級解決方案,說簡單也簡單,說難也難,就和solr一樣。配置這件事,要進階使用的話需要很大力氣花在源碼閱讀上,這樣你纔可以很好的進行定製和擴展。不然我們無法知道他給你寫好的簡單配置和複雜配置是怎麼實現的,我們應該使用哪些寫好的handler,需要什麼params。