电竞比分网-中国电竞赛事及体育赛事平台

分享

Shiro & CAS 實現(xiàn)單點登錄 | 凈土

 ebonyzhang 2016-07-19

Contents

概覽

單點登錄主要用于多系統(tǒng)集成,即在多個系統(tǒng)中,用戶只需要到一個中央服務器登錄一次即可訪問這些系統(tǒng)中的任何一個,無須多次登錄。

本文使用開源框架Jasig CAS來完成單點登錄。下載地址:https://www./cas/download。在寫本文時,使用的cas server版本為4.0.1

部署服務器

本文服務器使用Tomcat7,下載了cas-server-4.0.0-release.zip,將其解壓,找到modules目錄下面的cas-server-webapp-4.0.0.war直接復制到webapps文件夾下即可。啟動Tomcat,訪問http://localhost:8080/cas-server-webapp-4.0.0,使用casuser/Mellon登錄,即可登錄成功。

Tomcat默認沒有開啟HTTPS協(xié)議,所以這里直接用了HTTP協(xié)議訪問。為了能使客戶端在HTTP協(xié)議下單點登錄成功,需要修改一下配置:

  • WEB-INF\spring-configuration\ticketGrantingTicketCookieGenerator.xml和WEB-INF\spring-configuration\warnCookieGenerator.xml:將p:cookieSecure="true"改為p:cookieSecure="false"

  • WEB-INF\deployerConfigContext.xml:<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" />添加p:requireSecure="false"

至此,一個簡單的單點登錄服務器就基本部署好了。

部署客戶端

客戶端需要添加對shiro-cas和cas-client-core這兩個包的依賴。這里主要講跟CAS相關的配置。

之后配置web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 用于單點退出,該過濾器用于實現(xiàn)單點登出功能,可選配置。-->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 該過濾器用于實現(xiàn)單點登出功能,可選配置。 -->
<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>

自定義Realm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyCasRealm extends CasRealm {

private UserService userService;

public void setUserService(UserService userService) {
this.userService = userService;
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userService.findRoles(username));
authorizationInfo.setStringPermissions(userService.findPermissions(username));
return authorizationInfo;
}
}

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="casRealm" class="package.for.your.MyCasRealm">
<property name="userService" ref="userService"/>
<property name="cachingEnabled" value="true"/>
<property name="authenticationCachingEnabled" value="true"/>
<property name="authenticationCacheName" value="authenticationCache"/>
<property name="authorizationCachingEnabled" value="true"/>
<property name="authorizationCacheName" value="authorizationCache"/>
<!--該地址為cas server地址 -->
<property name="casServerUrlPrefix" value="${shiro.casServer.url}"/>
<!-- 該地址為是當前應用 CAS 服務 URL,即用于接收并處理登錄成功后的 Ticket 的,
必須和loginUrl中的service參數(shù)保持一致,否則服務器會判斷service不匹配-->
<property name="casService" value="${shiro.client.cas}"/>
</bean>

配置CAS過濾器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<property name="failureUrl" value="/casFailure.jsp"/>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="${shiro.login.url}"/>
<property name="successUrl" value="${shiro.login.success.url}"/>
<property name="filters">
<util:map>
<entry key="cas" value-ref="casFilter"/>
<entry key="logout" value-ref="logoutFilter" />
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/casFailure.jsp = anon
/cas = cas
/logout = logout
/** = user
</value>
</property>
</bean>

上面登錄url我的配置的是http://localhost:8080/cas-server/login?service=http://localhost:8080/cas-client/cas,service參數(shù)是之后服務將會跳轉的地址。

/cas=cas:即/cas 地址是服務器端回調(diào)地址,使用 CasFilter 獲取 Ticket 進行登錄。

之后通過eclipse部署,訪問http://localhost:8080/cas-client 即可測試。為了看到單點登錄的效果,可以直接復制一份webapps中的client為client2,只需要修改上述配置中的地址即可。如果用戶已經(jīng)登錄,那么訪問http://localhost:8080/cas-client2發(fā)現(xiàn)不會再跳轉到登錄頁面了,用戶已經(jīng)是登錄狀態(tài)了。

還需要注意一個問題,就是cas server默認是開啟單點登出的但是這里卻沒有這樣的效果,APP1登出了,但是APP2仍能訪問,如果查看瀏覽器的cookie的話,會發(fā)現(xiàn)有兩個sessionid,一個是JSESSIONID,容器原生的,另一個是shiro中配置的:

1
2
3
4
5
6
7
<!-- 會話Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
SingleSignOutFilter發(fā)現(xiàn)是logoutRequest請求后,原來SingleSignOutHandler中創(chuàng)建的原生的session已經(jīng)被銷毀了,因為從a登出的,a的shiro session也會銷毀,
但是b的shiro的session還沒有被銷毀,于是再訪問b還是能訪問,單點登出就有問題了-->
<constructor-arg value="JSESSIONID"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/>

如果我們把sid改為JSESSIONID會怎么樣,答案是如果改為JSESSIONID會導致重定向循環(huán),原因是當?shù)卿洉r,shiro發(fā)現(xiàn)瀏覽器發(fā)出的請求中的JSESSIONID沒有或已經(jīng)過期,于是生成一個JSESSIONID給瀏覽器,同時鏈接被重定向到服務器進行認證,認證成功后返回到客戶端服務器的cas service url,并且?guī)в幸粋€ticket參數(shù)。因為有SingleSignOutFilter,當發(fā)現(xiàn)這是一個tocken請求時,SingleSignOutHandler會調(diào)用request.getSession()獲取的是原生Session,如果沒有原生session的話,又會創(chuàng)建并將JSESSIONID保存到瀏覽器cookie中,當客戶端服務器向cas服務器驗證ticket之后,客戶端服務器重定向到之前的頁面,這時shiro發(fā)現(xiàn)JSESSIONID是SingleSignOutHandler中生成的,在自己維護的session中查不到,又會重新生成新的session,然后login,然后又會重定向到cas服務器認證,然后再重定向到客戶端服務器的cas service url,不同的是SingleSignOutHandler中這次調(diào)用session.getSession(true)不會新創(chuàng)建一個了,之后就如此循環(huán)。如果使用sid又會導致當單點登出時候,如果有a、b兩個客戶端服務器,從a登出,會跳轉到cas服務器登出,cas服務器會對所有通過它認證的service調(diào)用銷毀session的方法,但是b的shiro的session還沒有被銷毀,于是再訪問b還是能訪問,單點登出就有問題了

之所以這樣是因為我設置shiro的session管理器為DefaultWebSessionManager,這個管理器直接拋棄了容器的session管理器,自己來維護session,所以就會出現(xiàn)上述描述的問題了。如果我們不做設置,那么shiro將使用默認的session管理器ServletContainerSessionManager:Web 環(huán)境,其直接使用 Servlet 容器的會話。這樣單點登出就可以正常使用了。

此外如果我們非要使用DefaultWebSessionManager的話,我們就要重寫一個SingleSignOutFilter、SingleSignOutHandler和SessionMappingStorage了。

如果沒有使用Spring框架,則可以參考如下配置web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www./2001/XMLSchema-instance"
xmlns="http://java./xml/ns/javaee" xmlns:web="http://java./xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java./xml/ns/javaee http://java./xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>YPshop Authority Manage</display-name>
<context-param>
<param-name>webAppRootKey</param-name>
<param-value>authority.root</param-value>
</context-param>

<!-- ======================== 單點登錄開始 ======================== -->
<!-- 說明:這種客戶端的配置方式是不需要Spring支持的 -->
<!-- 參考資料:http://blog.csdn.net/yaoweijq/article/details/6003187 -->
<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>/*</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>https://localhost:8443/cas-server/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>https://localhost:8443</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/*</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>https://localhost:8443/cas-server</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>https://localhost:8443</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 該過濾器使得開發(fā)者可以通過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>/*</url-pattern>
</filter-mapping>
<!-- ======================== 單點登錄結束 ======================== -->

<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<distributable />
</web-app>

進階

使用HTTPS協(xié)議

首先我們需要生成數(shù)字證書

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
keytool -genkey -keystore "D:\localhost.keystore" -alias localhost -keyalg RSA
輸入密鑰庫口令:
再次輸入新口令:
您的名字與姓氏是什么?
[Unknown]: localhost
您的組織單位名稱是什么?
[Unknown]: xa
您的組織名稱是什么?
[Unknown]: xa
您所在的城市或區(qū)域名稱是什么?
[Unknown]: xi'an
您所在的省/市/自治區(qū)名稱是什么?
[Unknown]: xi'an
該單位的雙字母國家/地區(qū)代碼是什么?
[Unknown]: cn
CN=localhost, OU=xa, O=xa, L=xi'an, ST=xi'an, C=cn 是否正確

[否]: y
輸入 <localhost> 的密鑰口令
(如果和密鑰庫口令相同, 按回車):

需要注意的是 “您的名字與姓氏是什么?”這個地方不能隨便填的,如果運行過程中提示“Caused by: java.security.cert.CertificateException: No name matching localhost found”那么就是因為這里設置錯了,當然除了localhost也可以寫其他的,如helloworld.com,但是需要能解析出來,可以直接在hosts中加127.0.0.1 helloworld.com

然后,由于Tomcat默認沒有開HTTPS,所以我們需要在server.xml文件中找到8443出現(xiàn)的地方。然后修改如下

1
2
3
4
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
maxThreads="150" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS"
keystoreFile="D:\localhost.keystore" keystorePass="123456"/>

keystorePass 就是生成 keystore 時設置的密碼。

如果出現(xiàn)下面的問題,修改server.xml中的protocol為org.apache.coyote.http11.Http11Protocol

Failed to initialize end point associated with ProtocolHandler [“http-apr-8443”]
java.lang.Exception: Connector attribute SSLCertificateFile must be defined when using SSL with APR

因為 CAS client 需要使用該證書進行驗證,所以我們要使用 localhost.keystore 導出數(shù)字證書(公鑰)到 D:\localhost.cer。再將將證書導入到 JDK 中。

1
2
3
keytool -export -alias localhost -file D:\localhost.cer -keystore D:\localhost.keystore
cd D:\jdk1.7.0_21\jre\lib\security
keytool -import -alias localhost -file D:\localhost.cer -noprompt -trustcacerts -storetype jks -keystore cacerts -storepass 123456

如果導入失敗,可以先把 security 目錄下的 cacerts 刪掉

搞定證書之后,我們需要將之前client中配置的地址修改一下。然后還可以添加ssl過濾器。

如果遇到以下異常,一般是證書導入錯誤造成的,請嘗試重新導入,如果還是不行,有可能是運行應用的 JDK 和安裝數(shù)字證書的 JDK 不是同一個造成的:

Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

單點登出重定向

客戶端中配置logout過濾器

1
2
3
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="${shiro.logout.url}"/>
</bean>

WEB-INF/cas-servlet.xml中將 cas.logout.followServiceRedirects修改為true即可在登出后重定向到service參數(shù)提供的地址

單點登出

單點登出重定向是很好解決了,但是在客戶端與shiro集成過程中,如客戶端部署部分所述,如果shiro沒有使用 ServletContainerSessionManager 管理session,單點登出就會有問題了。最簡單奏效的辦法就是改用 ServletContainerSessionManager 了,但是我們偏要用 DefaultWebSessionManager 呢,那就應該要參考org.jasig.cas.client.session這個包中的幾個類,重新實現(xiàn)單點登出了。我的思路是,添加一個shiro過濾器,繼承自AdviceFilter在preHandle方法中實現(xiàn)邏輯:如果請求中包含了ticket參數(shù),記錄ticket和sessionID的映射;如果請求中包含logoutRequest參數(shù),標記session為無效;如果session不為空,且被標記為無效,則登出。如果請求中包含了logoutRequest參數(shù),那么這個請求是從cas服務器發(fā)出的,所以這里不能直接用subject.logout(),因為subject跟線程綁定,客戶端對cas服務器端的請求會創(chuàng)建一個新的subject。

那么CAS單點登出是怎么實現(xiàn)的呢,下面是我對CAS單點登出的簡單理解:

在TicketGrantingTicketImpl有一個HashMap services字段,以id和通過認證的客戶端service為鍵值對。當我們要登出時LogoutManagerImpl通過for (final String ticketId : services.keySet())向每個service發(fā)送一個POST請求,請求中包含一個logoutRequest參數(shù),參數(shù)的值由SamlCompliantLogoutMessageCreator創(chuàng)建。客戶端的 SingleSignOutFilter會判斷請求中是否包含了logoutRequest參數(shù),如果包含,那么銷毀session。SingleSignOutHttpSessionListener實現(xiàn)了javax.servlet.http.HttpSessionListener接口,用于監(jiān)聽session銷毀事件。

我在配置的過程中發(fā)現(xiàn)單點登出有問題,首先在服務端打開 debug log,cas 服務器默認是打開單點登出功能的,所以正常的話日志中會記錄<Sending logout request for: [https://localhost:8443/cas-client1/cas]>之類的內(nèi)容,有日志記錄發(fā)送了請求,一般服務器應該不會有什么問題了。那么有可能會是客戶端的問題,我重新配置了一個客戶端,這個客戶端沒有使用spring也沒有使用shiro,只用了在部署客戶端中提到的無spring的web.xml文件,發(fā)現(xiàn)從其他客戶端登出,這個客戶端也是登出的,所以這個配置是沒有什么問題。后來在瀏覽器打開控制臺才發(fā)現(xiàn)有兩個SESSIONID一個是sid是在shiro中配置的,另一個是JSESSIONID,應該是容器原生的。再然后就下了3.2.2版本的cas-client-core,通過maven構建,導入eclipse中,開始調(diào)試。我們的cas-client要依賴這個cas-client-core工程,怎么設置可以參考eclipse小技巧。然后調(diào)試,一定要保證在cas-client的propertie 設置中的Deployment Assembly中已經(jīng)沒有之前的版本的cas-client-core的jar包了。調(diào)試的過程中才發(fā)現(xiàn),SingleSignOutFilter銷毀的是容器原生的session,但是shiro的session還在,所以如果是從其他客戶端登出的,那這個客戶端還是能夠登錄。

通過數(shù)據(jù)庫中的用戶密碼認證

服務器端需要添加cas-server-support-jdbc和mysql-connector-java依賴。

cas-server-support-jdbc提供了org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler、org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler 和org.jasig.cas.adaptors.jdbc.QueryAndEncodeDatabaseAuthenticationHandler。他們都繼承自AbstractJdbcUsernamePasswordAuthenticationHandler 能夠通過配置sql語句驗證用戶憑證,后者更復雜些,能夠配置鹽,散列函數(shù)迭代次數(shù)。

下面說一下配置QueryDatabaseAuthenticationHandler,配置/src/main/webapp/WEB-INF/deployerConfigContext.xml,先注釋掉原先的primaryAuthenticationHandler然后添加下面配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 自定義數(shù)據(jù)庫鑒權 -->
<bean id="primaryAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<property name="dataSource" ref="dataSource"/>
<property name="sql" value="${auth.sql}"/>
<property name="passwordEncoder" ref="MD5PasswordEncoder"/>
</bean>

<!-- 數(shù)據(jù)源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${dataSource.driver}"></property>
<property name="url" value="${dataSource.url}"/>
<property name="username" value="${dataSource.username}"/>
<property name="password" value="${dataSource.password}"/>
</bean>
<!-- MD5加密 -->
<bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<constructor-arg value="MD5"/>
</bean>

加密算法可以自定義。

添加驗證碼

驗證碼的實現(xiàn)使用了kaptcha,所以需要添加其依賴。

web.xml添加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<servlet>
<servlet-name>Kaptcha</servlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
<!-- 設定寬度 -->
<init-param>
<param-name>kaptcha.image.width</param-name>
<param-value>100</param-value>
</init-param>
<!-- 設定高度 -->
<init-param>
<param-name>kaptcha.image.height</param-name>
<param-value>50</param-value>
</init-param>
<!-- 如果需要全部是數(shù)字 -->
<init-param>
<param-name>kaptcha.textproducer.char.string</param-name>
<param-value>0123456789</param-value>
</init-param>
<!-- 去掉干擾線 -->
<init-param>
<param-name>kaptcha.noise.impl</param-name>
<param-value>com.google.code.kaptcha.impl.NoNoise </param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Kaptcha</servlet-name>
<url-pattern>/captcha.jpg</url-pattern>
</servlet-mapping>

在login-webflow.xml中找到viewLoginForm,在binder節(jié)點下面添加<binding property="captcha" />,對應我們頁面提交的驗證碼參數(shù)

然后我們還要實現(xiàn)一個UsernamePasswordCaptchaCredential 類,繼承UsernamePasswordCredential 在其中添加了captcha字段和相應setter和getter方法。

1
2
3
4
5
6
7
8
public class UsernamePasswordCaptchaCredential extends UsernamePasswordCredential {
private static final long serialVersionUID = -2988130322912201986L;
@NotNull
@Size(min=1,message = "required.captcha")
private String captcha;

//set、get方法
}

接著回到 login-webflow.xml ,找到credential的聲明處,將org.jasig.cas.authentication.UsernamePasswordCredential修改為剛剛實現(xiàn)的類全路徑名。viewLoginForm 也需要修改

1
2
3
<transition on="submit" bind="true" validate="true" to="validatorCaptcha">
<evaluate ="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credential)" />
</transition>

再添加如下配置

1
2
3
4
5
6
<!-- 添加一個 validatorCaptcha 校驗驗證碼的操作 -->
<action-state id="validatorCaptcha">
<evaluate ="authenticationViaFormAction.validatorCaptcha(flowRequestContext, flowScope.credential, messageContext)"></evaluate>
<transition on="error" to="generateLoginTicket" />
<transition on="success" to="realSubmit" />
</action-state>

我們在配置中添加了一個 validatorCaptcha 的操作,同時可以看到 是 authenticationViaFormAction.validatorCaptcha(…),所以我們需要在 authenticationViaFormAction 中添加一個校驗驗證碼的方法 validatorCaptcha()。authenticationViaFormAction 這個bean是配置在 cas-servlet.xml 中的:

1
2
3
4
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
p:centralAuthenticationService-ref="centralAuthenticationService"
p:warnCookieGenerator-ref="warnCookieGenerator"
p:ticketRegistry-ref="ticketRegistry"/>

我們可以看看 org.jasig.cas.web.flow.AuthenticationViaFormAction 的源代碼,里面有一個 submit 方法,這個就是我們提交表單時的方法了。繼承AuthenticationViaFormAction實現(xiàn)一個新類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyAuthenticationViaFormAction extends AuthenticationViaFormAction{

public final String validatorCaptcha(final RequestContext context, final Credential credential,
final MessageContext messageContext){

final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
HttpSession session = request.getSession();
String captcha = (String)session.getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
session.removeAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);

UsernamePasswordCaptchaCredential upc = (UsernamePasswordCaptchaCredential)credential;
String submitAuthcodeCaptcha =upc.getCaptcha();


if(!StringUtils.hasText(submitAuthcodeCaptcha) || !StringUtils.hasText(submitAuthcodeCaptcha)){
messageContext.addMessage(new MessageBuilder().code("required.captcha").build());
return "error";
}
if(submitAuthcodeCaptcha.equals(captcha)){
return "success";
}
messageContext.addMessage(new MessageBuilder().code("error.authentication.captcha.bad").build());
return "error";
}
}

這邊有拋出兩個異常,這兩個異常信息 required.captcha、error.authentication.captcha.bad 需要在 messages_zh_CN.properties 文件下添加

1
2
required.captcha=必須輸入驗證碼。
error.authentication.captcha.bad=您輸入的驗證碼有誤。

然后把 authenticationViaFormAction 這個Bean路徑修改為我們新添加的類的全路徑名。

當然最后,我們的頁面也需要修改,找到casLoginView.jsp添加

1
2
3
4
5
6
<section class="row">
<spring:message code="screen.welcome.label.captcha.accesskey" var="captchaAccessKey" />
<spring:message code="screen.welcome.label.captcha" var="captchaHolder" />
<form:input cssClass="required" cssErrorClass="error" id="captcha" size="10" tabindex="3" path="captcha" placeholder="${captchaHolder }" accesskey="${captchaAccessKey}" autocomplete="off" htmlEscape="true" />
<img alt="${captchaHolder }" src="captcha.jpg" onclick="this.src='captcha.jpg?'+Math.random();">
</section>

以上添加驗證碼參考http://www.cnblogs.com/vhua/p/cas_3.html

添加記住密碼

可以參考http://jasig./cas/development/installation/Configuring-LongTerm-Authentication.html

在cas.properties中添加如下配置

1
2
# Long term authentication session length in seconds
rememberMeDuration=1209600

spring-configuration文件夾下找到 ticketExpirationPolicies.xml 和 ticketGrantingTicketCookieGenerator.xml 需要在這兩個配置文件中定義長期有效的session

在 ticketExpirationPolicies.xml文件中更新如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bean id="standardSessionTGTExpirationPolicy"
class="org.jasig.cas.ticket.support.TicketGrantingTicketExpirationPolicy"
p:maxTimeToLiveInSeconds="${tgt.maxTimeToLiveInSeconds:28800}"
p:timeToKillInSeconds="${tgt.timeToKillInSeconds:7200}"/>

<!--
| The following policy applies to long term CAS SSO sessions.
| Default duration is two weeks (1209600s).
-->
<bean id="longTermSessionTGTExpirationPolicy"
class="org.jasig.cas.ticket.support.TimeoutExpirationPolicy"
c:timeToKillInMilliSeconds="#{ ${rememberMeDuration:1209600} * 1000 }" />

<bean id="grantingTicketExpirationPolicy"
class="org.jasig.cas.ticket.support.RememberMeDelegatingExpirationPolicy"
p:sessionExpirationPolicy-ref="standardSessionTGTExpirationPolicy"
p:rememberMeExpirationPolicy-ref="longTermSessionTGTExpirationPolicy" />

更新ticketGrantingTicketCookieGenerator.xml

1
2
3
4
5
6
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="true"
p:cookieMaxAge="-1"
p:rememberMeMaxAge="${rememberMeDuration:1209600}"
p:cookieName="CASTGC"
p:cookiePath="/cas" />

在 deployerConfigContext.xml 中找到 PolicyBasedAuthenticationManager 使其包含RememberMeAuthenticationMetaDataPopulator組件

1
2
3
4
5
6
7
8
<property name="authenticationMetaDataPopulators">
<list>
<bean
class="org.jasig.cas.authentication.SuccessfulHandlerMetaDataPopulator" />
<bean
class="org.jasig.cas.authentication.principal.RememberMeAuthenticationMetaDataPopulator" />
</list>
</property>

和添加驗證碼類似的,我們還需要修改login-webflow.xml

找到credential 的聲明修改如下

1
<var name="credential" class="org.jasig.cas.authentication.RememberMeUsernamePasswordCredential" />

由于之前已經(jīng)實現(xiàn)了驗證碼,所以這里不需要修改了,只需讓 UsernamePasswordCaptchaCredential繼承RememberMeUsernamePasswordCredential即可

找到viewLoginForm 在binder節(jié)點下添加<binding property="rememberMe" />

更新 casLoginView.jsp

1
2
3
4
<section class="row check">
<input id="rememberMe" name="rememberMe" value="false" tabindex="4" accesskey="<spring:message code="screen.welcome.label.rememberMe.accesskey" />" type="checkbox" />
<label for="rememberMe"><spring:message code="screen.welcome.label.rememberMe" /></label>
</section>

自定義primaryAuthenticationHandler

雖然已經(jīng)有QueryDatabaseAuthenticationHandler和QueryAndEncodeDatabaseAuthenticationHandler兩個類,能夠通過配置sql語句驗證用戶憑證,后者還能配置鹽,散列函數(shù)迭代次數(shù)。但是我們可能還需要判斷用戶是否被鎖定或被禁用了,我們可以參考QueryAndEncodeDatabaseAuthenticationHandler自定義一個AuthenticationHandler,繼承AbstractJdbcUsernamePasswordAuthenticationHandler。添加兩個字段名lockedFieldName和disabledFieldName通過這兩個字段判斷用戶是否被鎖定或被禁用,關鍵代碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class ValidUserQueryDBAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler{
......
private static final String DEFAULT_LOCKED_FIELD = "locked";
private static final String DEFAULT_DISABLED_FIELD = "disabled";
......
@NotNull
protected String disabledFieldName = DEFAULT_DISABLED_FIELD;
@NotNull
protected String lockedFieldName = DEFAULT_LOCKED_FIELD;
......
public ValidUserQueryDBAuthenticationHandler(final DataSource datasource, final String sql, final String algorithmName) {
super();
setDataSource(datasource);
this.sql = sql;
this.algorithmName = algorithmName;
}

@Override
protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential transformedCredential)
throws GeneralSecurityException, PreventedException {
final String username = getPrincipalNameTransformer().transform(transformedCredential.getUsername());

try {
final Map<String, Object> values = getJdbcTemplate().queryForMap(this.sql, username);

if (Boolean.TRUE.equals(values.get(this.disabledFieldName))) {
throw new AccountDisabledException(username + " has been disabled.");
}
if (Boolean.TRUE.equals(values.get(this.lockedFieldName))) {
throw new AccountLockedException(username + " has been locked.");
}

final String digestedPassword = digestEncodedPassword(transformedCredential.getPassword(), values);
if (!values.get(this.passwordFieldName).equals(digestedPassword)) {
throw new FailedLoginException("Password does not match value on record.");
}
return createHandlerResult(transformedCredential,
new SimplePrincipal(username), null);

} 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);
}

}

protected String digestEncodedPassword(final String encodedPassword, final Map<String, Object> values) {
final ConfigurableHashService hashService = new DefaultHashService();

if (StringUtils.isNotBlank(this.staticSalt)) {
hashService.setPrivateSalt(ByteSource.Util.bytes(this.staticSalt));
}
hashService.setHashAlgorithmName(this.algorithmName);

Long numOfIterations = this.numberOfIterations;
if (values.containsKey(this.numberOfIterationsFieldName)) {
final String longAsStr = values.get(this.numberOfIterationsFieldName).toString();
numOfIterations = Long.valueOf(longAsStr);
}

hashService.setHashIterations(numOfIterations.intValue());
if (!values.containsKey(this.saltFieldName)) {
throw new RuntimeException("Specified field name for salt does not exist in the results");
}

final String dynaSalt = values.get(this.saltFieldName)==null?"":values.get(this.saltFieldName).toString();
final HashRequest request = new HashRequest.Builder()
.setSalt(dynaSalt)
.setSource(encodedPassword)
.build();
return hashService.computeHash(request).toHex();
}

public final void setDisabledFieldName(final String disabledFieldName) { this.disabledFieldName = disabledFieldName; }
public final void setLockedFieldName(final String lockedFieldName) { this.lockedFieldName = lockedFieldName; }
}

然后更新配置deployerConfigContext.xml

1
2
3
4
5
<bean id="primaryAuthenticationHandler" class="io.github.howiefh.cas.authentication.ValidUserQueryDBAuthenticationHandler">
<constructor-arg ref="dataSource" index="0"></constructor-arg>
<constructor-arg value="${auth.sql}" index="1"></constructor-arg>
<constructor-arg value="MD5" index="2"></constructor-arg>
</bean>

自定義登錄頁面

  1. 在cas.properties 修改 cas.viewResolver.basename 值為 custom_view ,那樣系統(tǒng)就會自動會查找 custom_view.properties 這個配置文件
  2. 直接復制原來的 default_views.properties 就行了,重命名為custom_view.properties
  3. 把 custom_view.properties 中的WEB-INF\view\jsp\default全部替換把這地址替換成 WEB-INF\view\jsp\custom
  4. 接下來把 cas\WEB-INF\view\jsp\default 下面的所有文件復制,然后重命名為我們需要的名稱,cas\WEB-INF\view\jsp\custom

主要修改casLoginView.jsp和cas.css即可

布局時遇到一個問題,就是將頁腳固定在頁面底部??梢詤⒖?a href="http://www./css/css-sticky-foot-at-bottom-of-the-page" target="_blank" rel="external">如何將頁腳固定在頁面底部

其它

【SSO單點系列】(4):CAS4.0 SERVER登錄后用戶信息的返回
在多點環(huán)境下使用cas實現(xiàn)單點登陸及登出
關于單點登錄中的用戶信息存儲問題的探討

原理

從結構來看,CAS主要分為Server和Client。Server主要負責對用戶的認證工作;Client負責處理客戶端受保護資源的訪問請求,登錄時,重定向到Server進行認證。

基礎模式的SSO訪問流程步驟:

  1. 訪問服務:客戶端發(fā)送請求訪問應用系統(tǒng)提供的服務資源。
  2. 定向認證:客戶端重定向用戶請求到中心認證服務器。
  3. 用戶認證:用戶進行身份認證
  4. 發(fā)放票據(jù):服務器會產(chǎn)生一個隨機的 Service Ticket 。
  5. 驗證票據(jù): SSO 服務器驗證票據(jù) Service Ticket 的合法性,驗證通過后,允許客戶端訪問服務。
  6. 傳輸用戶信息: SSO 服務器驗證票據(jù)通過后,傳輸用戶認證結果信息給客戶端。

CAS最基本的協(xié)議過程:

CAS 最基本的協(xié)議過程CAS 最基本的協(xié)議過程

如上圖: CAS Client 與受保護的客戶端應用部署在一起,以 Filter 方式保護 Web 應用的受保護資源,過濾從客戶端過來的每一個 Web 請求,同時, CAS Client 會分析 HTTP 請求中是否包含請求 Service Ticket( ST 上圖中的 Ticket) ,如果沒有,則說明該用戶是沒有經(jīng)過認證的;于是 CAS Client 會重定向用戶請求到 CAS Server ( Step 2 ),并傳遞 Service (要訪問的目的資源地址)。 Step 3 是用戶認證過程,如果用戶提供了正確的 Credentials , CAS Server 隨機產(chǎn)生一個相當長度、唯一、不可偽造的 Service Ticket ,并緩存以待將來驗證,并且重定向用戶到 Service 所在地址(附帶剛才產(chǎn)生的 Service Ticket ) , 并為客戶端瀏覽器設置一個 Ticket Granted Cookie ( TGC ) ; CAS Client 在拿到 Service 和新產(chǎn)生的 Ticket 過后,在 Step 5 和 Step6 中與 CAS Server 進行身份核實,以確保 Service Ticket 的合法性。

在該協(xié)議中,所有與 CAS Server 的交互均采用 SSL 協(xié)議,以確保 ST 和 TGC 的安全性。協(xié)議工作過程中會有兩次重定向的過程。但是 CAS Client 與 CAS Server 之間進行 Ticket 驗證的過程對于用戶是透明的(使用 HttpsURLConnection )。

相關概念

TGT、ST、PGT、PGTIOU、PT,其中TGT、ST是CAS1.0協(xié)議中就有的票據(jù),PGT、PGTIOU、PT是CAS2.0協(xié)議中有的票據(jù)。

CAS為用戶簽發(fā)登錄票據(jù),CAS認證成功后,將TGT對象放入自己的緩存,CAS生成cookie即TGC,自后登錄時如果有TGC的話,則說明用戶之前登錄過,如果沒有,則用戶需要重新登錄。

  • TGC (Ticket-granting cookie):存放用戶身份認證憑證的cookie,在瀏覽器和CAS Server用來明確用戶身份的憑證。
  • ST(Service Ticket):CAS服務器通過瀏覽器分發(fā)給客戶端服務器的票據(jù)。一個特定服務只能有一個唯一的ST。
  • PGT(Proxy Granting Ticket):由 CAS Server 頒發(fā)給擁有 ST 憑證的服務, PGT 綁定一個用戶的特定服務,使其擁有向 CAS Server 申請,獲得 PT 的能力。
  • PGTIOU(全稱 Proxy Granting Ticket I Owe You):作用是將通過憑證校驗時的應答信息由 CAS Server 返回給 CAS Client ,同時,與該 PGTIOU 對應的 PGT 將通過回調(diào)鏈接傳給 Web 應用。 Web 應用負責維護 PGTIOU 與 PGT 之間映射關系的內(nèi)容表。PGTIOU是CAS的serviceValidate接口驗證ST成功后,CAS會生成驗證ST成功的xml消息,返回給Proxy Service,xml消息中含有PGTIOU,proxy service收到Xml消息后,會從中解析出PGTIOU的值,然后以其為key,在map中找出PGT的值,賦值給代表用戶信息的Assertion對象的pgtId,同時在map中將其刪除。
  • PT(Proxy Ticket):是應用程序代理用戶身份對目標程序進行訪問的憑證;

CAS 基本流程圖(沒有使用PROXY代理)

CAS 基本流程圖(沒有使用PROXY代理)CAS 基本流程圖(沒有使用PROXY代理)

對于客戶端來說會通過客戶端session判斷用戶是否已認證,沒有的話跳轉到服務器認證,對于服務器,通過SSO session判斷用戶是否認證,沒有的話跳到登錄頁面。

CAS 基本流程圖(使用PROXY代理)

CAS 基本流程圖(使用PROXY代理)CAS 基本流程圖(使用PROXY代理)

這一節(jié)參考:

【SSO單點系列】(6):CAS4.0 單點流程序列圖(中文版)以及相關術語解釋(TGT、ST、PGT、PT、PGTIOU)
CAS實現(xiàn)SSO單點登錄原理

代碼:github

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多