SpringMVC + Shiro 集成 CAS

shiro 在 1.2 版本之后加入 shiro-cas 支持 sso 的 cas 登錄驗證,以下給出具體的對接方式

更多精彩

相關網址

  1. 從這里知道了 shiro.xml 的具體配置
  2. 講解了最基礎的通過 Filter 控制 CAS
  3. 可能會碰到的重定向問題
  4. 如何搭建 CAS Server
  5. GitHub - apereo/cas-overlay-template at 5.3

寫在前面的話

  1. CAS (Central Authentication Service) 是實現(xiàn) SSO (Single Sign On) [單點登錄] 的一個框架。還有其他框架,例如 Oauth
  2. SSO 的目的是實現(xiàn)多個應用系統(tǒng)共用一套登錄行為,在 Session 相同的前提下 [同一個瀏覽器] ,用戶進入不同系統(tǒng)只需要登錄一次

搭建 CAS Server

  1. 此處搭建 CAS Server 的原因并不是要實現(xiàn) 從客戶端請求到服務器認證 的全套邏輯,只是因為 新項目要接入到已經成型的 SSO 體系中
  2. 但是作為新項目在接入客戶的 SSO 體系時,很可能客戶不怎么配合工作(沒錯,這就是我碰到的情況),更不可能提供測試環(huán)境(沒錯,別說測試環(huán)境,到寫這篇筆記時,連生產環(huán)境的授權都沒通過)
  3. 所以對于之前沒有寫過 SSO 對接的菜雞(我自己),弄一個 CAS Server 作為測試服務器就至關重要了

下載 CAS Server 模版項目

  1. 如果只是作為測試服務器的話,CAS Server 不需要從零開始搭建服務器項目,直接前往 GitHub - apereo/cas-overlay-template at 5.3 下載即可
  2. 上述給出的鏈接是 5.3 版本,截止到寫筆記時,最新版本是 6.0
  3. 但最新版使用的是 gradle + jdk11 ,我的項目用的是 JDK7 ,本地環(huán)境也只下載了 JDK8 ,所以最后使用的 5.3 版本,使用的是 maven + jdk8
  4. 至于如何切換版本,看下圖


    源碼切換版本

編譯運行 CAS Server 模版項目

  1. 按照網站中提供的編譯方式 ./build.sh run 在項目根目錄執(zhí)行即可
    • 此處需要注意一點,就算本地環(huán)境中已經安裝了 maven ,在運行腳本時依舊會嘗試下載 ,而且實測非常慢
    • 解決方式是直接通過下載工具下載對應的 apache-maven-3.5.2-src.zip 丟到項目根目錄后,再執(zhí)行上述腳本,就可以直接下載成功并且編譯通過
  2. 編譯過程比較漫長,需要下載不少依賴包,全部的依賴包下載完畢后,在編譯過程中還會拋出各種異常,不用搭理,直接前往 /target 目錄獲取 cas.war 即可
  3. 將 cas.war 放置到 tomcat 的 webapps 目錄下后啟動 tomcat ,war 包就會自動解包并運行
  4. 通過瀏覽器訪問 http://127.0.0.1:8090/cas/login 可以直接進入 CAS Server 的登錄界面
    • 默認用戶名 casuser
    • 默認密碼 Mellon

為 CAS Server 添加 HTTP 許可

  1. 服務器默認并不支持 HTTP 請求,需要對配置文件做以下修改
    • 添加 HTTP 許可的原因是因為如果是 HTTPS 的話,需要編譯安全證書,這個過于繁瑣了,我們的搭建 CAS Server 的目的只是測試對接是否成功,所以沒必要搞那么復雜,直接選用 HTTP 即可
  2. 首先停止 tomcat,并前往 webapps 目錄找到解包后的 /cas 項目

修改 application.properties

  1. 具體地址 /cas/WEB-INF/classes/application.properties
  2. 在文件末尾添加以下代碼
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

  1. 具體地址 /cas/WEB-INF/classes/services/HTTPSandIMAPS-10000001.json
  2. 將內容直接替換成以下代碼,應該可以看到默認的 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)://.*"
  }
}

再次啟動服務查看修改結果

  1. 如果修改成功會顯示如下頁面
    • 右側黃色提示是表示沒有使用 HTTPS ,直接忽略
    • 右側第一個藍色提示是表示沒有使用 LDAP 或 JDBC 連接數(shù)據(jù)庫 ,導致目前用戶數(shù)據(jù)是寫死的,直接忽略(因為測試對接就已經足夠了)
    • 右側第二個藍色提示就是前文中修改 HTTPSandIMAPS-10000001.json 文件后生效的結果
      Cas Server 登錄界面

為等待對接的項目添加 CAS 支持

添加 POM 依賴

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

  1. 通常我們在使用 shiro 安全框架時,會編寫一個 ShiroDatabaseRealm ,繼承自 AuthorizingRealm ,用于在登錄時對用戶名密碼以及權限的自定義驗證
  2. 現(xiàn)在項目要通過 CAS 實現(xiàn) SSO ,說明用戶名密碼的驗證已經在 CAS Server 實現(xiàn),服務端驗證通過后返回到項目的是一個驗證通過的唯一標識
  3. 所以編寫一個 ShiroCasRealm ,繼承自 CasRealm ,來完成對 CAS Server 返回數(shù)據(jù)的驗證
  4. 以下代碼是具體實現(xiàn)邏輯,因為本項目沒有權限驗證提現(xiàn),所以 doGetAuthorizationInfo() 函數(shù)沒有重寫
  5. 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

  1. 具體到自己的項目時,不一定叫這個名字,反正就是 shiro 的配置文件

調用自定義 Realm

  1. memberServiceShiroCasRealm 中調用的 Service
    • MemberService.java 文件中添加 @Component("memberService") 實現(xiàn) Service 在容器加載時直接注入,這樣就不需要在顯式的通過 <bean/> 方式指定
  2. casServerUrlPrefix 是 CAS Server 的訪問地址
    • 此處使用的是本地測試環(huán)境,部署生產時替換為真實環(huán)境訪問地址即可,或者通過 <beans profile="dev"> 寫兩套配置
  3. 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 會話管理器

  1. shiroSessionDAO 是默認用于緩存 Session 的配置
  2. shiroSimpleCookie 是默認用戶保存 Cookie 的配置
    • SHAREJSESSIONID 是重寫了默認的 JSESSIONID 名稱
    • maxAge 賦值為 -1 是因為 實現(xiàn)單點登錄后項目本身應該不緩存用戶信息,CAS Server 用戶退出后,項目本身的用戶信息直接丟失
  3. 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 安全管理器

  1. casSubjectFactory 是默認的工廠類
  2. shiroCacheManager 是默認的緩存管理器
  3. 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 登錄過濾器

  1. 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 登出過濾器

  1. 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 通用過濾器

  1. loginUrl 是本項目初次訪問時會被重定向到 CAS Server 登錄頁,同時在參數(shù)中通過 service=http://127.0.0.1:8080/sop/login 指定登錄成功后回到本頁面的回到地址
    • service 中指定的地址必須與之前 casRealm 中指定的 casService 保持一致,否則會被服務端認為回調不匹配
  2. filters 中分別指定了 logoutFiltercasFilter 映射的別名,會在后續(xù)請求映射規(guī)則中中使用
  3. 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)項

  1. 這個就不解釋了
<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>

結語

  1. 按照上述操作依次配置后,項目本身就應該通過 CAS 與客戶現(xiàn)有的 SSO 體系對接成功
  2. 需要提到的是本次通過 CAS 對接 SSO ,由于原始項目已經使用了 shiro 作為安全框架,所有的配置都在 shiro.xml 中操作
    • 默認的如果沒有使用安全框架,那么 CAS 的配置則是在 web.xml 中完成的,那就是另一個故事了,此處不贅述
    • 友情提供一個普通版本通過 web.xml 配置的教程 普通模式通過 CAS 接入 SSO
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容