1. 程式人生 > >Shiro & CAS 實現單點登入

Shiro & CAS 實現單點登入

概覽

單點登入主要用於多系統整合,即在多個系統中,使用者只需要到一箇中央伺服器登入一次即可訪問這些系統中的任何一個,無須多次登入。

部署伺服器

Tomcat預設沒有開啟HTTPS協議,所以這裡直接用了HTTP協議訪問。為了能使客戶端在HTTP協議下單點登入成功,需要修改一下配置:

  • WEB-INF\spring-configuration\ticketGrantingTicketCookieGenerator.xmlp:cookieSecure="true" 改為 p:cookieSecure="false"

  • 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

<!-- 用於單點退出,該過濾器用於實現單點登出功能,可選配置。-->

< 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 >

自定義Realm:

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;

}

}

配置

< 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}" />

<!--必須和loginUrl中的service引數保持一致,否則伺服器會判斷service不匹配-->

< property name = "casService" value = "${shiro.client.cas}" />

</ bean >

配置CAS過濾器

<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引數是之後服務將會跳轉的地址。

/cas=cas :即/cas 地址是伺服器端回撥地址,使用 CasFilter 獲取 Ticket 進行登入。

還需要注意一個問題,就是cas server預設是開啟單點登出的但是這裡卻沒有這樣的效果,APP1登出了,但是APP2仍能訪問,如果檢視瀏覽器的cookie的話,會發現有兩個sessionid,一個是JSESSIONID,容器原生的,另一個是shiro中配置的:

<!-- 會話Cookie模板 -->

< bean id = "sessionIdCookie" class = "org.apache.shiro.web.servlet.SimpleCookie">

SingleSignOutFilter發現是logoutRequest請求後,原來SingleSignOutHandler中建立的原生的session已經被銷燬了,因為從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會導致重定向迴圈,原因是當登入時,shiro發現瀏覽器發出的請求中的JSESSIONID沒有或已經過期,於是生成一個JSESSIONID給瀏覽器,同時連結被重定向到伺服器進行認證,認證成功後返回到客戶端伺服器的cas service url,並且帶有一個ticket引數。因為有SingleSignOutFilter,當發現這是一個tocken請求時,SingleSignOutHandler會呼叫request.getSession()獲取的是原生Session,如果沒有原生session的話,又會建立並將JSESSIONID儲存到瀏覽器cookie中,當客戶端伺服器向cas伺服器驗證ticket之後,客戶端伺服器重定向到之前的頁面,這時shiro發現JSESSIONID是SingleSignOutHandler中生成的,在自己維護的session中查不到,又會重新生成新的session,然後login,然後又會重定向到cas伺服器認證,然後再重定向到客戶端伺服器的cas service url,不同的是SingleSignOutHandler中這次呼叫session.getSession(true)不會新建立一個了,之後就如此迴圈。如果使用sid又會導致當單點登出時候,如果有a、b兩個客戶端伺服器,從a登出,會跳轉到cas伺服器登出,cas伺服器會對所有通過它認證的service呼叫銷燬session的方法,但是b的shiro的session還沒有被銷燬,於是再訪問b還是能訪問,單點登出就有問題了

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

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

  如果沒有使用Spring框架,則可以參考如下配置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 > YPshop Authority Manage </ display-name >

< context-param >

< param-name > webAppRootKey </ param-name >

< param-value > authority.root </ param-value >

</ context-param >

<!-- 說明:這種客戶端的配置方式是不需要Spring支援的 -->

< 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 >

< 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協議

首先我們需要生成數字證書

keytool -genkey -keystore "D:\localhost.keystore" -alias localhost -keyalg RSA

輸入金鑰庫口令:

再次輸入新口令:

您的名字與姓氏是什麼?

[ Unknown ]: localhost

您的組織單位名稱是什麼?

[Unknown]: xa

您的組織名稱是什麼?

[Unknown]: xa

您所在的城市或區域名稱是什麼?

[Unknown]: xi'an

您所在的省/市/自治區名稱是什麼?

[Unknown]: xi'an

該單位的雙字母國家/地區程式碼是什麼?

[Unknown]: cn

CN=localhost, OU=sishuok.com, O=sishuok.com, L=beijing, ST=beijing, 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出現的地方。然後修改如下

<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 時設定的密碼。

如果出現下面的問題,修改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 匯出數字證書(公鑰)到 D:\localhost.cer。再將將證書匯入到 JDK 中。

keytool -export -alias localhost -file D: \localhost .cer -keystore D: \localhost .keystore

cd D: \jdk 1.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 和安裝數字證書的 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

自定義登入頁面

  1. 在cas.properties 修改 cas.viewResolver.basename 值為 custom_view ,那樣系統就會自動會查詢 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即可

佈局時遇到一個問題,就是將頁尾固定在頁面底部。可以參看 如何將頁尾固定在頁面底部

其它

原理

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

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

  1. 訪問服務:客戶端傳送請求訪問應用系統提供的服務資源。
  2. 定向認證:客戶端重定向使用者請求到中心認證伺服器。
  3. 使用者認證:使用者進行身份認證
  4. 發放票據:伺服器會產生一個隨機的 Service Ticket 。
  5. 驗證票據: SSO 伺服器驗證票據 Service Ticket 的合法性,驗證通過後,允許客戶端訪問服務。
  6. 傳輸使用者資訊: SSO 伺服器驗證票據通過後,傳輸使用者認證結果資訊給客戶端。

CAS最基本的協議過程:


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

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