CAS 是 Yale 大學發起的一個開源項目,旨在爲 Web 應用系統提供一種可靠的單點登陸方法,CAS 在 2004 年 12 月正式成爲 JA-SIG 的一個項目。CAS 具備如下特色:java
從結構上看,CAS 包含兩個部分: CAS Server 和 CAS Client。CAS Server 須要獨立部署,主要負責對用戶的認證工做;CAS Client 負責處理對客戶端受保護資源的訪問請求,須要登陸時,重定向到 CAS Server。下圖 是 CAS 最基本的協議過程:web
在圖中第3步用戶認證成功後,cas server會生成Ticket Granting Ticket(票據受權票據,簡稱TGT),同時將TGT值以CASTGC爲名保存到瀏覽器的cookie中(注意:CASTGC保存在cas server的cookie中,在登陸成功以後 若是咱們再次訪問 http://cas-server/login 能夠從cookie中看到這個CASTGC的值),以後生成Service Ticket(服務票據,簡稱ST)並緩存,在第4步時將ST經過瀏覽器重定向的URL傳給cas client。spring
當cas client驗證ST時,是在後臺請求cas server驗證ST,而cas server在已緩存的ST中查找是否存在cas client傳來的ST,若存在則返回驗證成功同時將該ST刪除(這就保證了用同一ST不能反覆進入client應用,同時這也是爲何不直接將TGT返回給cas client的緣由)。sql
那麼名爲CASTGC的cookie起什麼做用呢?瀏覽器
當客戶訪問另外一個cas client時,一樣會被重定向到cas server,而此時咱們並不但願再次讓用戶輸入用戶密碼登錄,名爲CASTGC的cookie這時就體現出做用來了,cas server發現存在名爲CASTGC的cookie就將其值在已保存的TGT中查找,若存在,則說明已存在合法的TGT,cas server就根據該TGT生成新的ST,接下來的流程就和之前同樣了。緩存
CAS 協議中還提供了 Proxy (代理)模式,以適應更加高級、複雜的應用場景,具體介紹能夠參考 CAS 官方網站上的相關文檔。tomcat
首先下載已配置好的代碼 http://www.oschina.net/code/snippet_559410_58170安全
而後在這個項目基礎上配置,配置步驟以下:服務器
1 加載本身的配置文件cookie
修改propertyFileConfigurer.xml 文件來增長本身的配置文件
<bean id="propertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <!-- 本身的配置文件 --> <value>classpath*:system.properties</value> <value>/WEB-INF/cas.properties</value> </list> </property> </bean>
2 配置數據源dataSource
修改deployerConfigContext.xml文件,配置數據源:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>${jdbc.driverClassName}</value> </property> <property name="url"> <value>${jdbc.url}</value> </property> <property name="username"> <value>${jdbc.username}</value> </property> <property name="password"> <value>${jdbc.password}</value> </property> </bean>
3 自定義的登陸驗證
根據本身的登陸驗證業務編寫QueryDatabaseAuthenticationHandler:
public class QueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler { protected final Logger log = LoggerFactory.getLogger(this.getClass()); @NotNull private String sql; /** {@inheritDoc} */ @Override protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential) throws GeneralSecurityException, PreventedException { final String username = credential.getUsername(); try { Member member = findMember(username); if (member == null ) { throw new AccountNotFoundException(username + " not found"); } final String encPass = getPasswordEncoder().encode(credential.getPassword()); if (!member.getPassword().equals(encPass)) { throw new FailedLoginException("Password does not match value on record."); } } catch (final IncorrectResultSizeDataAccessException e) { if (e.getActualSize() == 0) { throw new AccountNotFoundException(username + " not found with SQL query"); } else { throw new FailedLoginException("Multiple records found for " + username); } } catch (final DataAccessException e) { throw new PreventedException("SQL exception while executing query for " + username, e); } return createHandlerResult(credential, new SimplePrincipal(username), null); } /** * @param sql The sql to set. */ public void setSql(final String sql) { this.sql = sql; } private Member findMember(String username) { String sql = "select * from member where username = ?"; Member member = null; try { member = (Member) getJdbcTemplate().queryForObject(sql, new RowMapper<Object>() { @Override public Object mapRow(ResultSet resultSet, int arg1) { Member member = new Member(); try { member.setId(resultSet.getInt("id")); member.setEmail(resultSet.getString("email")); member.setUsername(resultSet.getString("username")); member.setMobile(resultSet.getString("mobile")); member.setPassword(resultSet.getString("password")); } catch (SQLException e) { e.printStackTrace(); member = null; } return member; } }, username); } catch (Exception e) { log.error(e.getMessage()); } return member; } }
在deployerConfigContext.xml文件配置QueryDatabaseAuthenticationHandler類
<bean id="passwordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" p:characterEncoding="UTF-8"> <constructor-arg index="0" value="MD5" /> </bean> <bean id="primaryAuthenticationHandler" class="com.cas.jdbc.QueryDatabaseAuthenticationHandler"> <property name="dataSource" ref="dataSource"></property> <property name="sql" value="select password from operator where account=?"></property> <property name="passwordEncoder" ref="passwordEncoder"></property> </bean>
4 修改登陸驗證提示信息
修改messages.properties裏面的內容,須要將英文提示改爲中文提示,而且把中文提示信息轉爲國際化編碼,即中文轉Unicode編碼,核心提示代碼以下:
# Authentication failure messages authenticationFailure.AccountDisabledException=This account has been disabled. authenticationFailure.AccountLockedException=This account has been locked. authenticationFailure.CredentialExpiredException=Your password has expired. authenticationFailure.InvalidLoginLocationException=You cannot login from this workstation. authenticationFailure.InvalidLoginTimeException=Your account is forbidden to login at this time. authenticationFailure.AccountNotFoundException=\u8d26\u53f7\u6216\u5bc6\u7801\u4e0d\u6b63\u786e\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002 authenticationFailure.FailedLoginException=\u8d26\u53f7\u6216\u5bc6\u7801\u4e0d\u6b63\u786e\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002 authenticationFailure.UNKNOWN=\u8d26\u53f7\u6216\u5bc6\u7801\u4e0d\u6b63\u786e\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002
5 自定義登陸頁面
修改casLoginView.jsp頁面 儘可能保持原來的jsp標籤引入不變,而後根據本身的設計增長登陸相關代碼
6 自定義登出業務
用戶退出登陸,是須要傳service值(這個是登出後的跳轉地址)。
例如登出的請求爲http://localhost:8082/cas-server-webapp/login?service=http://www.baidu.com
還有一個問題:cas4.0默認對http請求方式不是安全registeredServicesList裏面的,因此須要修改配置文件deployerConfigContext.xml 在registeredServicesList中增長http的請求。
<util:list id="registeredServicesList"> <bean class="org.jasig.cas.services.RegexRegisteredService" p:id="0" p:name="HTTP and IMAP" p:description="Allows HTTP(S) and IMAP(S) protocols" p:serviceId="^(https?|imaps?|http?)://.*" p:evaluationOrder="10000001" /> <!-- Use the following definition instead of the above to further restrict access to services within your domain (including sub domains). Note that example.com must be replaced with the domain you wish to permit. This example also demonstrates the configuration of an attribute filter that only allows for attributes whose length is 3. --> </util:list>
雖然cas是做爲開源單點登陸解決方案的一個不錯選擇,可是官方提供的缺省實現代碼卻不併支持cas server多點部署以及每一個cas client多點部署的狀況。這在現今愈來愈強調服務穩定性的潮流下,多少顯得有些不合時宜。那麼是否是服務是集羣部署就不能使用cas了呢?答案是否認的。
對cas server來講,默認實現不能支持多點部署的緣由在於TGT保存時使用的ticket register類將TGT保存在了Java類的變量中。相關配置以下:
WEB-INF\spring-configuration\ticketRegistry.xml:
<bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.DefaultTicketRegistry" />
若是要支持多點部署,咱們能夠經過引入memcached的方式,在多點環境下仍然可以正常使用cas。咱們能夠新建立一個類(如MemcachedTicketRegistry)實現AbstractTicketRegistry接口,修改相關配置以下:
WEB-INF\spring-configuration\ticketRegistry.xml:(在此省略了memcachedClient的配置)
<bean id="ticketRegistry" class="com.xxx.cas.server.MemcachedTicketRegistry"> <property name="client" ref="memcachedClient" /> </bean>
這樣cas server已經能夠多點部署了,然而此時咱們會發現單點登出功能不正常了。經過debug和查看代碼,發現TGT中保存的service集合爲空,這是單點登出不正常的直接緣由,由於cas server會遍歷TGT中保存的service集合,依次向對應的cas client發出退出請求。然而爲何TGT中保存的service集合會爲空呢?這是由於TGT從第一次被保存到memcached後就再也沒有被保存到memcached,這樣從memcached中取得的TGT天然仍是最初的TGT,固然其中的service會爲空了,而cas默認實現中TGT是始終保持在內存中的天然不會有問題。既然找到了問題的緣由就簡單了,咱們只要每當TGT增長service後,再次將TGT保存到memcached就能解決這個問題。
WEB-INF\spring-configuration\applicationContext.xml修改以下:
<bean id="centralAuthenticationService" class="org.jasig.cas.CentralAuthenticationServiceImpl" ... /> 替換爲 <bean id="centralAuthenticationService" class="com.xxx.cas.server.CentralAuthenticationServiceImpl" ... />
CentralAuthenticationServiceImpl類相比org.jasig.cas.CentralAuthenticationServiceImpl類有以下不一樣:
this.serviceTicketRegistry.addTicket(serviceTicket); //com.xxx.cas.server.CentralAuthenticationServiceImp類增長了下面一行: this.serviceTicketRegistry.addTicket(ticketGrantingTicket);
Cas Client 配置請參考下面的文件連接