前面學習實踐了CAS服務端的配置和登陸驗證等內容,下面要進行單點登錄的應用子系統接入的實踐。單點登錄最大的使用場景就是解決多個子系統的多次登陸問題,使用CAS框架可以將多次登陸化簡爲統一一次登陸認證。
1、服務端配置service
客戶端接入 CAS 首先需要在服務端進行註冊,否則客戶端訪問將提示“未認證授權的服務”警告:
我們可以參考原來的service配置進行修改,原來的配置文件在war解壓後的如下位置:
將這個文件拷貝到工程中的resources/services下,打開這個文件,如下圖
分析一下里面的內容
{
//這是註冊類,除非自己實現了一個註冊類,否則不用動
"@class" : "org.apereo.cas.services.RegexRegisteredService",
//匹配url
"serviceId" : "^(https|imaps)://.*",
//服務的全局名稱
"name" : "HTTPS and IMAPS",
//服務的id
"id" : 10000001,
//描述:描述客戶端的服務
"description" : "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
//定義多個服務的執行順序
"evaluationOrder" : 10000
}
}
Json文件的命名必須滿足如下規則:
${name}-${id}.json 其中 id 與文件中的id一致,名稱
下面我們參考上面的內容建立一個要加入到單點登錄的子應用的配置文件
名字就設定爲APP1-10000003.json
然後在配置文件application.properties下添加配置:
##
# Service Registry(服務註冊)
#
# 開啓識別Json文件,默認false
cas.serviceRegistry.initFromJson=true
#自動掃描服務配置,默認開啓
#cas.serviceRegistry.watcherEnabled=true
#120秒掃描一遍
cas.serviceRegistry.schedule.repeatInterval=120000
#延遲15秒開啓
# cas.serviceRegistry.schedule.startDelay=15000
##
# Json配置
cas.serviceRegistry.json.location=classpath:/services
添加完成後,啓動CAS服務端,可以看到有2個service已經啓動。本例是因爲刪除了原來的配置json,否則的話,會看到4個services
啓動日誌看到,有兩個service,因爲默認的war包下面還有一個service。看到app1
已經註冊成功
2、 客戶端1(業務系統1)接入
2.1 準備一個客戶端工程
首先我們準備一個待接入的客戶端1,我們命名爲app1,下面我們先將app1工程調起來,本例子中使用的是一個基於maven的簡單SSM工程。這個工程,通過從數據庫中獲取賬號和密碼進行登錄驗證,這個工程的詳細內容,已經放到github上,可以從github的下面url 獲取這個初始工程:https://github.com/cwqsolo/StudySsm.git
這個工程,沒有集成cas客戶端時,運行起來的登陸界面如下圖所示:
完成後,初始工作完成後,我們開始客戶端應用1的接入。注意應用1的運行端口配置如下
啓動應用1以後,打開url:http://localhost:8380/login.jsp 用賬號admin 密碼 123456
下面我們需要在此工程的基礎上添加基於cas的單點登錄。好,讓我們開始吧。
2.2 導入證書
客戶端證書和服務端證書是同一個證書,不然就會報錯,我因爲是在同一臺機器,所以就沒有進行以下操作。
sudo keytool -import -file E:/tmp/tomcat-key/tomcat.cer -alias tomcat -keys
2.3 修改pom.xml 文件
首先修改pom.xml 文件,添加cas客戶端依賴
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.5.0</version>
</dependency>
如下圖:
添加完成後,重新導入依賴。可以採用下面的方式用右鍵方式進行重新導入
2.4 修改web.xml
1)增加displayname
2)添加單點登錄的相關內容
注意這裏的單點登錄的內容中涉及到的服務端url和客戶端url需要和實際的一致
<!-- ========================單點登錄開始 ======================== -->
<!-- 用於單點退出,該過濾器用於實現單點登出功能,可選配置 -->
<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>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://server.cas.com:8080/cas</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 該過濾器用於實現單點登錄功能 -->
<filter>
<filter-name>CAS Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://server.cas.com:8080/cas/login</param-value>
<!-- 使用的CAS-Server的登錄地址,一定是到登錄的action -->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://app1.cas.com:8380/node1/login.jsp</param-value>
<!-- 當前Client系統的地址 -->
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 該過濾器負責對Ticket的校驗工作 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://server.cas.com:8080/cas</param-value>
<!-- 使用的CAS-Server的地址,一定是在瀏覽器輸入該地址能正常打開CAS-Server的根地址 -->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://app1.cas.com:8380/node1/login.jsp</param-value>
<!-- 當前Client系統的地址 -->
</init-param>
<!--<init-param>-->
<!--<param-name>redirectAfterValidation</param-name>-->
<!--<param-value>true</param-value>-->
<!--</init-param>-->
<init-param>
<param-name>useSession</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</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>/*</url-pattern>
</filter-mapping>
<!--
該過濾器使得開發者可以通過org.jasig.cas.client.util.AssertionHolder來獲取用戶的登錄名。
比如AssertionHolder.getAssertion().getPrincipal().getName()
或者request.getUserPrincipal().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>
<!-- ========================單點登錄結束 ======================== -->
2.5 單點退出
原來客戶端的登出是在controller(本例是userController.java)中,在該文件中,將退出重新指向login.jsp。 現在使用cas單點登出後,需要到服務端的退出,這樣就實現了單點登出。
2.6 啓動客戶端1:
客戶端的url是http://app1.cas.com:8380/login.jsp, 賬號和密碼爲 admin, 123456。賬號和密碼登陸後,展示的url是:http://app1.cas.com:8380/index.jsp。
我們預期的結果是:
1)當輸入http://app1.cas.com:8380/index.jsp頁面的時候,如果當前系統沒有登錄,則跳轉到cas進行登錄,登錄後,打開index頁面。
2)登陸成功後,在app1中進行業務操作跳轉時,和原來app1應用一樣。
3)單點登出:執行完成後,退出系統,會退出cas服務端,這樣下次再訪問客戶端1的時候,會要求繼續輸入賬號和密碼
下面我們分別在cas服務端沒有開啓和開啓的情況下分別查看一下單點登錄SSO的情況是否符合我們的預期。
2.6.1 CAS服務未啓動
首先測試一下如果cas服務端沒有開啓的情況下,啓動工程會如何。啓動完成後,我打開上面的應用1的url,彈出下面的網頁,說明應用1去連接cas服務端,但是沒有連接上。
2.6.2 開啓CAS服務端
在確保CAS服務端正確開啓的情況下,我們輸入客戶端1(app1)的url,http://app1.cas.com:8380/index.jsp (這個頁面是正常登陸後的頁面),這個時候,客戶端1(app1)沒有彈出自己的登陸頁面,而是彈出cas的登陸界面
在登錄界面的右上角,顯示出當前登陸的是app1.
我們輸入app1的賬號 admin 和密碼 123456,這個時候,會跳出來下面的界面
看到這個界面說明客戶端應用1已經通過單點登錄登錄到應用。之後,在系統中的測試都和使用原系統一致。
我們現在測試一下退出登錄,點擊退出登錄-退出後顯示下面的界面:
說明退出客戶端,並且在服務端也註銷了。這個時候訪問客戶端的應用http://app1.cas.com:8380/index.jsp 界面,又會彈出單點登錄的界面。說明客戶端1的單點登錄和單點登出已經被cas服務端(統一登錄)全面管理了。
2.6.4 重要說明
如果客戶端的賬號,因爲某種原因刪除後,而沒有同步刪除統一登錄服務端的賬號,則該賬號還是會可以進入到客戶端系統,因此賬號的同步非常重要,另外客戶端應用界面要對用戶進行二次檢查。
修改後的工程也可以在github上獲取:
https://github.com/cwqsolo/StudySsmCas.git
3、客戶端2(採用shiro)接入
下面我們在另外一個客戶端2(app2)上實現cas單點登錄和單點登出。客戶端2和客戶端1的區別在於客戶端2集成了shiro進行管理。客戶端2的工程也可以在github上找到:
https://github.com/cwqsolo/StudySsmShiro.git
集成了cas後的工程再github上鍊接如下
https://github.com/cwqsolo/StudySsmShiroCas.git
3.1 修改pom.xml 文件
首先修改pom.xml 文件,由於shiro集成了cas,所以可以添加如下依賴
<!-- shiro-cas集成依賴包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.4.0</version>
</dependency>
添加完成後,重新導入依賴。可以採用右鍵方式進行重新導入
3.2 修改web.xml
由於基礎工程已經是一個shiro工程,所以在web.xml中已經含有shiro相關配置,無需修改
3.3 修改spring-shiro.xml
修改spring-shiro.xml 文件,使得其適用於cas應用。修改的地方主要是一些地址的跳轉,完整的文件信息如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">
<!-- shiro的核心配置: 配置shiroFileter id名必須與web.xml中的filtername保持一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- 要求登錄時的鏈接(可根據項目的URL進行替換),非必須的屬性,默認會自動尋找Web工程根目錄下的"/login.html"頁面 -->
<property name="loginUrl" value="http://server.cas.com:8080/cas/login?service=http://app2.cas.com:8580/node2/shiro-cas" />
<property name="filters">
<map>
<!-- 添加casFilter到shiroFilter, 這裏的key cas 需要和下面的/shiro-cas = cas 一致 -->
<entry key="cas" value-ref="casFilter" />
<entry key="logout" value-ref="logoutFilter" />
</map>
</property>
<!--/shiro-cas 是回調地址,不需要實現,指向了casFilter /logout = logout-->
<property name="filterChainDefinitions">
<value>
/shiro-cas = cas
/unauthorized.jsp = anon
/index.jsp = authc
/user/** = user
</value>
</property>
</bean>
<!-- CasFilter爲自定義的單點登錄Fileter -->
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<!-- 配置驗證錯誤時的失敗頁面 -->
<property name="failureUrl" value="/unauthorized.jsp"/>
<property name="successUrl" value="/user/loginSuccess" />
</bean>
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<!-- 配置驗證錯誤時的失敗頁面 -->
<property name="redirectUrl" value="http://server.cas.com:8080/cas/logout" />
</bean>
<!-- 單點登錄下的配置 -->
<bean id="casRealm" class="com.studyssm.shiro.MyRealm">
<property name="defaultRoles" value="ROLE_USER"/>
<!-- cas服務端地址前綴 -->
<property name="casServerUrlPrefix" value="http://server.cas.com:8080/cas" />
<!-- 應用服務地址,用來接收cas服務端票據 -->
<!-- 客戶端的回調地址(函數),必須和上面的shiro-cas過濾器casFilter攔截的地址一致 -->
<property name="casService" value="http://app2.cas.com:8580/node2/shiro-cas" />
</bean>
<!-- 配置安全管理器securityManager, 緩存技術: 緩存管理 realm:負責獲取處理數據 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="subjectFactory" ref="casSubjectFactory"></property>
<property name="realm" ref="casRealm" />
<property name="cacheManager" ref="cacheManager" />
</bean>
<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"></bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!-- 配置緩存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager" />
<!-- 保證實現了Shiro內部lifecycle函數的bean執行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<bean
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod"
value="org.apache.shiro.SecurityUtils.setSecurityManager"></property>
<property name="arguments" ref="securityManager"></property>
</bean>
</beans>
<
3.5 修改MyRealm.java
修改shiro的自定義realm,添加cas認證方面的內容,具體realm類MyRealm.java內容如下:
package com.studyssm.shiro;
import com.studyssm.entity.User;
import com.studyssm.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasAuthenticationException;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.util.StringUtils;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
/**
* Describe:
*
* @author cwqsolo MyRealm ,繼承自 CasRealm ,來完成對 CAS Server 返回數據的驗證
* @date 2019/07/22
*/
public class MyRealm extends CasRealm {
@Autowired
private UserService userService;
private User us;
/**
* 授權,在配有緩存的情況下,只加載一次。
* @param principal
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("Myrealm doGetAuthenticationInfo 1++++");
//當前登錄用戶,賬號
us= (User)principal.getPrimaryPrincipal();
String username = us.getUserName();
System.out.println("當前登錄用戶:"+username);
//獲取角色信息
Set<String> roles = userService.findRoles(username);
if(roles.size()==0){
System.out.println("當前用戶沒有角色!");
}else
{
}
SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
simpleAuthenticationInfo.setRoles(userService.findRoles(username));
simpleAuthenticationInfo.setStringPermissions(userService.findPermissions(username));
return simpleAuthenticationInfo ;
}
/**
* 認證登錄,查詢數據庫,如果該用戶名正確,得到正確的數據,並返回正確的數據
* AuthenticationInfo的實現類SimpleAuthenticationInfo保存正確的用戶信息
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("Myrealm 認證 ++++1");
CasToken casToken = (CasToken) token;
// token爲空直接返回,頁面會重定向到 Cas Server 登錄頁,並且攜帶本項目回調頁
if (token == null) {
System.out.println("Myrealm 認證 token 爲空 ++++");
return null;
}
System.out.println("Myrealm 認證 ++++2");
// 獲取服務端範圍的票根
String ticket = (String) casToken.getCredentials();
// 票根爲空直接返回,頁面會重定向到 Cas Server 登錄頁,並且攜帶本項目回調頁
if (!StringUtils.hasText(ticket)) {
System.out.println("Myrealm 認證 ticket 爲空 ++++");
return null;
}
TicketValidator ticketValidator = ensureTicketValidator();
try {
System.out.println("Myrealm 認證 ++++4");
// 票根驗證
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
// 獲取服務端返回的用戶數據
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
System.out.println("Myrealm 認證 ++++5");
// 拿到用戶唯一標識
String username = casPrincipal.getName();
// 通過唯一標識查詢數據庫用戶表
// 如果查詢到對應用戶則直接返回用戶數據
us = userService.findUserByUsername(username);
System.out.println("Myrealm 認證 us info="+us.toString());
//如果沒有查詢到,拋出異常
if( us == null ) {
System.out.println("Myrealm::賬戶"+username+"不存在!");
throw new UnknownAccountException("賬戶"+username+"不存在!");
}else{
//如果查詢到了,封裝查詢結果,
Object principal = us.getUserName();
Object credentials = us.getPassword();
String realmName = this.getName();
// 將獲取到的本項目數據庫用戶包裝爲 shiro 自身的 principal 存於當前 session 中
// 之後在整個項目中都可以通過 SecurityUtils.getSubject().getPrincipal() 直接獲取到當前用戶信息
List<Object> principals = CollectionUtils.asList(us, casPrincipal.getAttributes());
PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
System.out.println("Myrealm 認證 return sucessfully!!!");
return new SimpleAuthenticationInfo(principalCollection, ticket);
}
} catch (TicketValidationException e) {
System.out.println("Myrealm 認證 ++++++");
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
}
3.6 單點退出
原來客戶端的登出是在controller(本例是userController.java)中,在該文件中,將退出重新指向login.jsp。 現在使用cas單點登出後,需要到服務端的退出,這樣就實現了單點登出。userController.java類可以從github上獲取,這裏貼出退出函數:
/**
* 退出
*
* @param session
* @return
*/
@RequestMapping("/logout")
public String logout(HttpSession session) {
System.out.println("run logout....");
//session.invalidate();
Subject subject = SecurityUtils.getSubject();
//判斷當前用戶是否已登錄
if (subject.isAuthenticated()) {
//退出登錄
subject.logout();
System.out.println("subject.logout!!!");
}
return "redirect:http://server.cas.com:8080/cas/logout?service=http://app2.cas.com:8580/node2/shiro-cas";
}
4、跨系統單點登錄驗證
4.1 啓動客戶端2:
客戶端的url是http://app2.cas.com:8580/login.jsp, 賬號和密碼爲 admin, 123456。賬號和密碼登陸後,展示的url是:http://app2.cas.com:8580/index.jsp。
我們預期的結果是:
1)當輸入http://app2.cas.com:8580/index.jsp頁面的時候,如果當前系統沒有登錄,則跳轉到cas進行登錄,登錄後,打開index頁面。
2)登陸成功後,在app1中進行業務操作跳轉時,和原來應用一樣。
3)單點登出:執行完成後,退出系統,會退出cas服務端,這樣下次再訪問客戶端1的時候,會要求繼續輸入賬號和密碼
4.2 應用1和應用2都沒有登錄
在確保CAS服務端正確開啓的情況下,我們分別在兩個瀏覽器界面兩個客戶端應用的url
app1.cas.com:8380/node1/index.jsp
app2.cas.com:8580/node2/index.jsp
這個時候,瀏覽器會跳轉到單點登錄服務端,顯示登陸界面,兩個登陸界面的區別是分別有app1和app2提示說明是哪個客戶端應用接入
應用2的登陸界面
4.3 應用1登陸,應用2無需登陸
這個時候,我們在應用1登陸,再打開應用2,就無需登陸了。
使用admin賬號登陸應用1(app1)
這個時候,刷新一下應用2界面,發現也進入系統了
4.4 應用1登出,應用2也無法訪問
接下來,我們將應用1登出,如下圖
然後我們再訪問應用2時,會出現重新登錄界面。
4.5 應用1和應用2的賬號差異
當某賬號在應用1中存在,在應用2中不存在的時候,進行登錄訪問,則應用1可以正常訪問,應用2會提示
5 重要說明
如果客戶端的賬號,因爲某種原因刪除後,而沒有同步刪除統一登錄服務端的賬號,則該賬號還是會可以進入到客戶端系統,因此賬號的同步非常重要,另外客戶端應用界面要對用戶進行二次檢查。