shiro 在 1.2 版本之后加入 shiro-cas 支持 sso 的 cas 登錄驗證,以下給出具體的對接方式
更多精彩
- 更多技術博客,請移步 IT人才終生實訓與職業(yè)進階平臺 - 實訓在線
相關網址
- 從這里知道了 shiro.xml 的具體配置
- 講解了最基礎的通過 Filter 控制 CAS
- 可能會碰到的重定向問題
- 如何搭建 CAS Server
- GitHub - apereo/cas-overlay-template at 5.3
寫在前面的話
- CAS (Central Authentication Service) 是實現(xiàn) SSO (Single Sign On) [單點登錄] 的一個框架。還有其他框架,例如 Oauth
- SSO 的目的是實現(xiàn)多個應用系統(tǒng)共用一套登錄行為,在 Session 相同的前提下 [同一個瀏覽器] ,用戶進入不同系統(tǒng)只需要登錄一次
搭建 CAS Server
- 此處搭建 CAS Server 的原因并不是要實現(xiàn) 從客戶端請求到服務器認證 的全套邏輯,只是因為 新項目要接入到已經成型的 SSO 體系中
- 但是作為新項目在接入客戶的 SSO 體系時,很可能客戶不怎么配合工作(沒錯,這就是我碰到的情況),更不可能提供測試環(huán)境(沒錯,別說測試環(huán)境,到寫這篇筆記時,連生產環(huán)境的授權都沒通過)
- 所以對于之前沒有寫過 SSO 對接的菜雞(我自己),弄一個 CAS Server 作為測試服務器就至關重要了
下載 CAS Server 模版項目
- 如果只是作為測試服務器的話,CAS Server 不需要從零開始搭建服務器項目,直接前往 GitHub - apereo/cas-overlay-template at 5.3 下載即可
- 上述給出的鏈接是 5.3 版本,截止到寫筆記時,最新版本是 6.0
- 但最新版使用的是 gradle + jdk11 ,我的項目用的是 JDK7 ,本地環(huán)境也只下載了 JDK8 ,所以最后使用的 5.3 版本,使用的是 maven + jdk8
-
至于如何切換版本,看下圖
源碼切換版本
編譯運行 CAS Server 模版項目
- 按照網站中提供的編譯方式
./build.sh run在項目根目錄執(zhí)行即可- 此處需要注意一點,就算本地環(huán)境中已經安裝了 maven ,在運行腳本時依舊會嘗試下載 ,而且實測非常慢
- 解決方式是直接通過下載工具下載對應的 apache-maven-3.5.2-src.zip 丟到項目根目錄后,再執(zhí)行上述腳本,就可以直接下載成功并且編譯通過
- 編譯過程比較漫長,需要下載不少依賴包,全部的依賴包下載完畢后,在編譯過程中還會拋出各種異常,不用搭理,直接前往 /target 目錄獲取 cas.war 即可
- 將 cas.war 放置到 tomcat 的 webapps 目錄下后啟動 tomcat ,war 包就會自動解包并運行
- 通過瀏覽器訪問
http://127.0.0.1:8090/cas/login可以直接進入 CAS Server 的登錄界面- 默認用戶名 casuser
- 默認密碼 Mellon
為 CAS Server 添加 HTTP 許可
- 服務器默認并不支持 HTTP 請求,需要對配置文件做以下修改
- 添加 HTTP 許可的原因是因為如果是 HTTPS 的話,需要編譯安全證書,這個過于繁瑣了,我們的搭建 CAS Server 的目的只是測試對接是否成功,所以沒必要搞那么復雜,直接選用 HTTP 即可
- 首先停止 tomcat,并前往 webapps 目錄找到解包后的 /cas 項目
修改 application.properties
- 具體地址
/cas/WEB-INF/classes/application.properties - 在文件末尾添加以下代碼
cas.tgc.secure=false
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.watcherEnabled=true
cas.serviceRegistry.schedule.repeatInterval=120000
cas.serviceRegistry.schedule.startDelay=15000
cas.serviceRegistry.managementType=DEFAULT
cas.serviceRegistry.json.location=classpath:/services
cas.logout.followServiceRedirects=true
修改 HTTPSandIMAPS-10000001.json
- 具體地址
/cas/WEB-INF/classes/services/HTTPSandIMAPS-10000001.json - 將內容直接替換成以下代碼,應該可以看到默認的 serviceId 只有
^(https|imaps)://.*
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|imaps|http)://.*",
"name" : "測試服務器",
"id" : 10000001,
"description" : "測試一下CAS連接",
"evaluationOrder" : 10000,
"proxyPolicy" : {
"@class" : "org.jasig.cas.services.RegexMatchingRegisteredServiceProxyPolicy",
"pattern" : "^(https|imaps|http)://.*"
}
}
再次啟動服務查看修改結果
- 如果修改成功會顯示如下頁面
- 右側黃色提示是表示沒有使用 HTTPS ,直接忽略
- 右側第一個藍色提示是表示沒有使用 LDAP 或 JDBC 連接數(shù)據(jù)庫 ,導致目前用戶數(shù)據(jù)是寫死的,直接忽略(因為測試對接就已經足夠了)
- 右側第二個藍色提示就是前文中修改 HTTPSandIMAPS-10000001.json 文件后生效的結果
Cas Server 登錄界面
為等待對接的項目添加 CAS 支持
添加 POM 依賴
- 在 pom.xml 中添加以下依賴
- shiro-cas 是 shiro 自 1.2 版本后添加的對 CAS 的官方實現(xiàn)
- cas-client-core 是 CAS 的核心包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.2.1</version>
</dependency>
編寫 ShiroCasRealm
- 通常我們在使用 shiro 安全框架時,會編寫一個 ShiroDatabaseRealm ,繼承自 AuthorizingRealm ,用于在登錄時對用戶名密碼以及權限的自定義驗證
- 現(xiàn)在項目要通過 CAS 實現(xiàn) SSO ,說明用戶名密碼的驗證已經在 CAS Server 實現(xiàn),服務端驗證通過后返回到項目的是一個驗證通過的唯一標識
- 所以編寫一個 ShiroCasRealm ,繼承自 CasRealm ,來完成對 CAS Server 返回數(shù)據(jù)的驗證
- 以下代碼是具體實現(xiàn)邏輯,因為本項目沒有權限驗證提現(xiàn),所以
doGetAuthorizationInfo()函數(shù)沒有重寫 -
memberService.getMemberByCas(userId)是項目接入服務端用戶體系的關鍵步驟- 在沒有接入之前項目本身有就已經有自己完整的用戶體系,項目內部其他的需求邏輯都是圍繞項目自身的用戶體系搭建
- 所以在接入服務端用戶體系時,就需要通過服務端返回的用戶唯一標識來創(chuàng)建一份自己的用戶,同時保證自身用戶和服務端用戶一對一,類似于平臺用戶綁定微信賬戶后可以通過微信掃碼直接登錄
public class ShiroCasRealm extends CasRealm {
private MemberServiceImpl memberService;
public void setMemberService(MemberServiceImpl memberService) {
this.memberService = memberService;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 沒有權限驗證體系,所以直接返回
return super.doGetAuthorizationInfo(principals);
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CasToken casToken = (CasToken) token;
// token為空直接返回,頁面會重定向到 Cas Server 登錄頁,并且攜帶本項目回調頁
if (token == null) {
return null;
}
// 獲取服務端范圍的票根
String ticket = (String) casToken.getCredentials();
// 票根為空直接返回,頁面會重定向到 Cas Server 登錄頁,并且攜帶本項目回調頁
if (!StringUtils.hasText(ticket)) {
return null;
}
TicketValidator ticketValidator = ensureTicketValidator();
try {
// 票根驗證
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
// 獲取服務端返回的用戶數(shù)據(jù)
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
// 拿到用戶唯一標識
String userId = casPrincipal.getName();
// 通過唯一標識查詢數(shù)據(jù)庫用戶表
// 如果查詢到對應用戶則直接返回用戶數(shù)據(jù)
// 如果沒有查詢到用戶數(shù)據(jù)則向數(shù)據(jù)庫新增用戶并返回用戶數(shù)據(jù)
MemberDTO member = memberService.getMemberByCas(userId);
// 將獲取到的本項目數(shù)據(jù)庫用戶包裝為 shiro 自身的 principal 存于當前 session 中
// 之后在整個項目中都可以通過 SecurityUtils.getSubject().getPrincipal() 直接獲取到當前用戶信息
List<Object> principals = CollectionUtils.asList(member, casPrincipal.getAttributes());
PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
return new SimpleAuthenticationInfo(principalCollection, ticket);
} catch (TicketValidationException e) {
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
}
}
改寫 applicationContext-shiro.xml
- 具體到自己的項目時,不一定叫這個名字,反正就是 shiro 的配置文件
調用自定義 Realm
-
memberService 是 ShiroCasRealm 中調用的 Service
- 在 MemberService.java 文件中添加
@Component("memberService")實現(xiàn) Service 在容器加載時直接注入,這樣就不需要在顯式的通過 <bean/> 方式指定
- 在 MemberService.java 文件中添加
-
casServerUrlPrefix 是 CAS Server 的訪問地址
- 此處使用的是本地測試環(huán)境,部署生產時替換為真實環(huán)境訪問地址即可,或者通過
<beans profile="dev">寫兩套配置
- 此處使用的是本地測試環(huán)境,部署生產時替換為真實環(huán)境訪問地址即可,或者通過
-
casService 是 CAS Server 登錄成功后回到本項目的回調地址
- 必須與后續(xù)的 loginUrl 中的后半段保持一直,否則會被服務端認為回調不匹配
- 此處使用的同樣是本地測試環(huán)境,部署生產時需要替換為真實環(huán)境地址
<bean id="casRealm" class="com.innovaee.ppts.common.security.ShiroCasRealm">
<property name="memberService" ref="memberService"/>
<property name="casServerUrlPrefix" value="http://127.0.0.1:8090/cas"/>
<property name="casService" value="http://127.0.0.1:8080/sop/login"/>
</bean>
配置 SessionManager 會話管理器
- shiroSessionDAO 是默認用于緩存 Session 的配置
-
shiroSimpleCookie 是默認用戶保存 Cookie 的配置
- SHAREJSESSIONID 是重寫了默認的 JSESSIONID 名稱
- maxAge 賦值為 -1 是因為 實現(xiàn)單點登錄后項目本身應該不緩存用戶信息,CAS Server 用戶退出后,項目本身的用戶信息直接丟失
-
sessionManager 是默認的會話管理器
- globalSessionTimeout 賦值為 -1 是因為 實現(xiàn)單點登錄后項目本身應該不限制用戶 Session 存放時間 ,項目的 Session 直接從 CAS Server 獲取
- sessionValidationSchedulerEnabled 賦值為 true ,表示依舊驗證 Session 有效性
<bean id="shiroSessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"/>
<bean id="shiroSimpleCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="SHAREJSESSIONID"/>
<property name="maxAge" value="-1"/>
</bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="-1"/>
<property name="sessionDAO" ref="shiroSessionDAO"/>
<property name="sessionIdCookie" ref="shiroSimpleCookie"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
</bean>
配置 SecurityManager 安全管理器
- casSubjectFactory 是默認的工廠類
- shiroCacheManager 是默認的緩存管理器
-
securityManager 是默認的安全管理器
- realm 指定為前文中編寫的 casRealm
<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"/>
<bean id="shiroCacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="casRealm"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="shiroCacheManager"/>
<property name="subjectFactory" ref="casSubjectFactory"/>
</bean>
配置 CasFilter 登錄過濾器
-
casFilter 是 shiro 官方實現(xiàn)的 CAS 登錄規(guī)則過濾器,我們只需要調用并填寫失敗與成功的回調地址即可
- failureUrl 表示登錄失敗后會返回到 CAS Server 登錄頁,同時攜帶再次登錄成功后的本項目登錄頁
- successUrl 表示登錄成功后訪問本項目的根目錄
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<property name="failureUrl" value="http://127.0.0.1:8090/cas/login?service=http://127.0.0.1:8080/sop/login"/>
<property name="successUrl" value="/app/home"/>
</bean>
配置 LogoutFilter 登出過濾器
-
logoutFilter 是 shiro 官方實現(xiàn)的 CAS 登出規(guī)則過濾器,只需要調用并填寫重定向的回調地址即可
- redirectUrl 表示用戶在本項目中執(zhí)行登出操作后,會重定向到 CAS Server 的登出頁,同時攜帶再次登錄成功后的本項目登錄頁
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="http://127.0.0.1:8090/cas/logout?service=http://127.0.0.1:8080/sop/login"/>
</bean>
配置 ShiroFilter 通用過濾器
-
loginUrl 是本項目初次訪問時會被重定向到 CAS Server 登錄頁,同時在參數(shù)中通過
service=http://127.0.0.1:8080/sop/login指定登錄成功后回到本頁面的回到地址-
service中指定的地址必須與之前 casRealm 中指定的 casService 保持一致,否則會被服務端認為回調不匹配
-
- filters 中分別指定了 logoutFilter 和 casFilter 映射的別名,會在后續(xù)請求映射規(guī)則中中使用
-
filterChainDefinitions 中指定了各種請求會進入哪些過濾器
- 此處的
/login = cas非常關鍵,正是因為此處標明只有/login請求會進入 casFilter - 所以在上述所有的 CAS Server 登錄成功后回到本項目的回調地址中都攜帶了
/login請求 - 這并不是因為本項目需要再次進入登錄頁面進行登錄,而是因為需要通過 casFilter 進行一次登錄規(guī)則驗證
- 如果項目提供給 CAS Server 的回調地址默認不會經過 casFilter ,那么在 Cas Server 登錄成功后就可以導致重復重定向
- 此處的
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="http://127.0.0.1:8090/cas/login?service=http://127.0.0.1:8080/sop/login"/>
<property name="successUrl" value="/"/>
<property name="filters">
<map>
<entry key="logout" value-ref="logoutFilter"/>
<entry key="cas" value-ref="casFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<value>
/logout = logout
/login = cas
/** = user,perms,roles
</value>
</property>
</bean>
配置 Shiro 與 Spring 關聯(lián)項
- 這個就不解釋了
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
結語
- 按照上述操作依次配置后,項目本身就應該通過 CAS 與客戶現(xiàn)有的 SSO 體系對接成功
- 需要提到的是本次通過 CAS 對接 SSO ,由于原始項目已經使用了 shiro 作為安全框架,所有的配置都在 shiro.xml 中操作
- 默認的如果沒有使用安全框架,那么 CAS 的配置則是在 web.xml 中完成的,那就是另一個故事了,此處不贅述
- 友情提供一個普通版本通過 web.xml 配置的教程 普通模式通過 CAS 接入 SSO

