使用Spring Security 集成 CAS 完成單點登錄

一、企業(yè)單一登錄(CAS)

1.Java(Spring Webflow / MVC servlet)服務器組件

2.可插拔認證支持(LDAP,數(shù)據(jù)庫,X.509,雙因素)

3.支持多種協(xié)議(CAS,SAML,OAuth,OpenID)

4.跨平臺的客戶端支持(Java,.Net,PHP,Perl,Apache等)

5.與uPortal,Liferay,BlueSocket,Moodle和Google Apps集成,僅舉幾例

CAS提供了一個友好的開源社區(qū),積極支持和貢獻項目。雖然該項目植根于更高級的開放源代碼,但已經發(fā)展成為世界500強企業(yè)和小型專用設施的國際用戶。

二、如何部署您的CAS

在項目中安裝CAS服務器,需要去官方github下載CAS標準WAR文件,在WAR文件中有標準的單點登錄登出頁面。當然您還需要對deployerConfigContext.xml中指定AuthenticationHandler進行簡單的修改,已滿足您對數(shù)據(jù)庫的操作需求。CAS 本身包含大量的AuthenticationHandler,可以協(xié)助解決相應的問題。

除CAS服務器本身之外,其他關鍵角色當然是在企業(yè)中部署的安全Web應用程序,這些Web應用程序被稱為“服務“。有三種類型的服務:驗證服務票據(jù),獲得代理票據(jù),驗證代理票據(jù)。驗證代理票據(jù)的不同之處在于代理列表必須經過驗證,并且通常可以重用代理。

CAS本身設計在HTTPS環(huán)境下,在本地測試以及個人學習情況下可以對CAS做些相應修改,使它支持HTTP訪問。在CAS的WAR文件目錄:WEB-INF\classes\services下修改HTTPSandIMAPS-10000001.json配置文件,將serviceId屬性的值修改為:

"serviceId":"^(https|imaps|http)://.*"

在CAS 4.2版本后,CAS的所有配置都放在cas.properties文件中,所以為了可以自定義cas.properties的路徑,您可以修改WEB-INF\spring-configuration\propertyFileConfigurer.xml文件中的:

<util:properties id="casProperties" location="classpath:cas.properties" />

為了讓CAS能夠通過數(shù)據(jù)庫鑒定用戶憑證,需要配置Database Authentication。官方文檔詳見:https://apereo.github.io/cas/4.2.x/installation/Database-Authentication.html。數(shù)據(jù)庫認證有四種:

1.QueryDatabaseAuthenticationHandler,通過用戶名和明文密碼進行驗證。
首先在cas.properties中配置:
# cas.jdbc.authn.query.sql=select password from users where username=?
在deployerConfigContext.xml中配置
<alias name="queryDatabaseAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="dataSource" alias="queryDatabaseDataSource" />

2.SearchModeSearchDatabaseAuthenticationHandler,通過查詢用戶名和密碼來搜索用戶記錄; 如果至少有一個結果被發(fā)現(xiàn),用戶將被認證。
首先在cas.properties中配置
# cas.jdbc.authn.search.password=
# cas.jdbc.authn.search.user=
# cas.jdbc.authn.search.table=
在deployerConfigContext.xml中配置
<alias name="searchModeSearchDatabaseAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="dataSource" alias="searchModeDatabaseDataSource" />

 3.BindModeSearchDatabaseAuthenticationHandler,嘗試使用用戶名和(散列)密碼創(chuàng)建數(shù)據(jù)庫連接來對用戶進行身份驗證。
在deployerConfigContext.xml中配置
<alias name="bindModeSearchDatabaseAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="dataSource" alias="bindSearchDatabaseDataSource" />

4.QueryAndEncodeDatabaseAuthenticationHandler,一個JDBC查詢處理程序,它將撤回用戶的密碼和私有salt值,并使用公共salt值驗證編碼的密碼。 假設一切都在同一個數(shù)據(jù)庫表內。 支持迭代次數(shù)和私鹽的設置。
首先在cas.properties中配置
# cas.jdbc.authn.query.encode.sql=
# cas.jdbc.authn.query.encode.alg=
# cas.jdbc.authn.query.encode.salt.static=
# cas.jdbc.authn.query.encode.password=表字段名
# cas.jdbc.authn.query.encode.salt=表字段名
# cas.jdbc.authn.query.encode.iterations.field=表字段名
# cas.jdbc.authn.query.encode.iterations=
在deployerConfigContext.xml中配置
<alias name="queryAndEncodeDatabaseAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="dataSource" alias="queryEncodeDatabaseDataSource" />

一般選擇第四種數(shù)據(jù)認證方式,修改完成后丟到tomcat下運行即可。
cas的訪問地址:ip:port/cas/login
cas的登出地址:ip:port/cas/logout

三、Spring Security和CAS的集成

Web瀏覽器,CAS服務器和Spring安全服務之間的基本交互如下:

CAS或Spring Security不管理公共頁面的處理,當用戶請求一個安全的頁面或者它使用的一個安全的頁面。 Spring Security的ExceptionTranslationFilter將檢測到AccessDeniedException或AuthenticationException。

由于用戶的Authentication對象(或缺少)導致AuthenticationException,因此ExceptionTranslationFilter將調用已配置的AuthenticationEntryPoint。如果使用CAS,這將是CasAuthenticationEntryPoint類。

CasAuthenticationEntryPoint將把用戶的瀏覽器重定向到CAS服務器。它還會顯示一個服務參數(shù),它是Spring Security服務(您的應用程序)的回調URL。例如,瀏覽器重定向到的URL可能是

https://my.company.com/cas/login?service= HTTPS%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin / CAS。

用戶的瀏覽器重定向到CAS后,系統(tǒng)會提示用戶輸入用戶名和密碼。如果用戶提交了一個表示他們以前登錄過的會話cookie,他們將不會再被提示重新登錄(這個過程是個例外,我們將在后面介紹)。 CAS將使用上述的PasswordHandler(或使用CAS 3.0的AuthenticationHandler)來決定用戶名和密碼是否有效。

CAS成功登錄后,會將用戶瀏覽器重定向到原始服務。它還將包含一個票據(jù)參數(shù),這是一個不透明的字符串,代表“服務票據(jù)”。繼續(xù)前面的例子,瀏覽器被重定向到的URL可能是

https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ。

回到服務Web應用程序,CasAuthenticationFilter總是監(jiān)聽/ login / cas的請求(這是可配置的,但是我們將使用這個介紹中的默認值)。處理過濾器將構建代表服務票據(jù)的UsernamePasswordAuthenticationToken。主體將等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而憑證將是服務票證不透明值。這個認證請求將被交給配置的AuthenticationManager。

AuthenticationManager實現(xiàn)將是ProviderManager,它又被配置了CasAuthenticationProvider。 CasAuthenticationProvider只響應包含CAS特定主體(如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和CasAuthenticationToken(稍后討論)的UsernamePasswordAuthenticationToken。

CasAuthenticationProvider將使用TicketValidator實現(xiàn)來驗證服務票證。這通常是一個Cas20ServiceTicketValidator,它是包含在CAS客戶端庫中的一個類。如果應用程序需要驗證代理票證,則使用Cas20ProxyTicketValidator。 TicketValidator向CAS服務器發(fā)出HTTPS請求,以驗證服務票據(jù)。它也可能包含一個代理回調URL,它包含在這個例子中:

https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket= ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl = HTTPS://server3.company.com/webapp/login/cas/proxyreceptor。

回到CAS服務器,驗證請求將被接收。如果所提供的服務票據(jù)與發(fā)行票據(jù)的服務URL相匹配,則CAS將以XML表示用戶名的肯定響應。如果任何代理參與了身份驗證(如下所述),那么代理列表也會包含在XML響應中。

[可選]如果對CAS驗證服務的請求包含代理回調URL(在pgtUrl參數(shù)中),則CAS將在XML響應中包含一個pgtIou字符串。這pgtIou代表代理授予票借條。然后,CAS服務器將創(chuàng)建自己的HTTPS連接回pgtUrl。這是為了相互認證CAS服務器和聲稱的服務URL。 HTTPS連接將用于將授權票據(jù)的代理發(fā)送到原始Web應用程序。例如,

  https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH。

Cas20TicketValidator將解析從CAS服務器收到的XML。它將返回CasAuthenticationProvider TicketResponse,其中包括用戶名(強制),代理列表(如果有任何涉及),和代理授予票證IOU(如果代理回調被請求)。

接下來,CasAuthenticationProvider將調用已配置的CasProxyDecider。 CasProxyDecider指示TicketResponse中的代理列表是否可以被服務接受。 Spring Security提供了幾個實現(xiàn):RejectProxyTickets,AcceptAnyCasProxy和NamedCasProxyDecider。這些名稱在很大程度上是不言而喻的,除了NamedCasProxyDecider允許提供可信代理列表。

CasAuthenticationProvider接下來將請求一個AuthenticationUserDetailsS??ervice來加載適用于Assertion中包含的用戶的GrantedAuthority對象。
如果沒有問題,CasAuthenticationProvider構造一個CasAuthenticationToken,包括TicketResponse和GrantedAuthoritys中包含的細節(jié)。

控制然后返回到CasAuthenticationFilter,它將創(chuàng)建的CasAuthenticationToken放置在安全上下文中。

用戶的瀏覽器被重定向到導致AuthenticationException的原始頁面(或根據(jù)配置的自定義目標)。

四、Spring Boot +Spring Security+CAS開發(fā)(代理票據(jù)認證)

CasAuthenticationProvider區(qū)分有狀態(tài)和無狀態(tài)客戶端。 有狀態(tài)的客戶端被認為是提交給CasAuthenticationFilter的filterProcessUrl的。 無狀態(tài)客戶端是指向除FilterProcessUrl以外的URL向CasAuthenticationFilter提交身份驗證請求的任何客戶端。

由于遠程協(xié)議無法在HttpSession的上下文中呈現(xiàn),因此不可能依賴于在請求之間的會話中存儲安全上下文的默認實踐。 此外,由于CAS服務器在TicketValidator驗證之后使其無效,因此在后續(xù)請求中顯示相同的代理票證將不起作用。

CasConfing配置:
//客戶端配置
public static String casServiceHost="http://127.0.0.1:8080";
public static String casServiceLogin=casServiceHost+"/login/cas";
public static String casServiceLogout=casServiceHost+"/logout/cas";
public static String casServiceProxyCallbackUrl="/login/cas/proxyreceptor";
public static String casServiceFailureHandler="/cas/casfailed";

//cas服務端配置
@Value("${cas.server.host:http://127.0.0.1:8081/cas}")
public static String casServerUrlPrefix="http://127.0.0.1:8081/cas";
public static String casServerUrlLogin=casServerUrlPrefix+"/login";
public static String casServerUrlLogout=casServerUrlPrefix+"/logout";

@Autowired
public static ProxyGrantingTicketStorageImpl pgtStorage;

@Bean
public ServiceProperties serviceProperties(){
    ServiceProperties serviceProperties=new ServiceProperties();
    serviceProperties.setService(casServiceLogin);
    serviceProperties.setAuthenticateAllArtifacts(true);
    return serviceProperties;
}

@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint(@Qualifier("serviceProperties") ServiceProperties serviceProperties){
    CasAuthenticationEntryPoint entryPoint=new CasAuthenticationEntryPoint();
    entryPoint.setServiceProperties(serviceProperties);
    entryPoint.setLoginUrl(casServerUrlLogin);

    return entryPoint;
}

@Bean("pgtStorage")
public ProxyGrantingTicketStorageImpl proxyGrantingTicketStorageImpl(){
    return new ProxyGrantingTicketStorageImpl();
}

@Bean("casAuthenticationProvider")
public CasAuthenticationProvider casAuthenticationProvider(@Qualifier("serviceProperties") ServiceProperties serviceProperties,
        @Qualifier("customCasUserDetailsService") CustomCasUserDetailsService customCasUserDetailsService){
    CasAuthenticationProvider authenticationProvider=new CasAuthenticationProvider();
    authenticationProvider.setKey("casProvider") ;
    authenticationProvider.setServiceProperties(serviceProperties);
    Cas20ProxyTicketValidator ticketValidator=new Cas20ProxyTicketValidator(casServerUrlPrefix);
    ticketValidator.setAcceptAnyProxy(true);//允許所有代理回調鏈接
    ticketValidator.setProxyGrantingTicketStorage(pgtStorage);
    authenticationProvider.setTicketValidator(ticketValidator);
    authenticationProvider.setAuthenticationUserDetailsService(customCasUserDetailsService);
    //無狀態(tài)緩存
    EhCacheBasedTicketCache ticketCache=new EhCacheBasedTicketCache();
    ticketCache.setCache(new Cache("casTickets", 50, true, false, 3600, 900));
    authenticationProvider.setStatelessTicketCache(ticketCache);
    
    return authenticationProvider;
}

//單點登出,跳轉到客戶端的登出鏈接
@Bean("requestSingleLogoutFilter")
public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter(casServerUrlLogout, new SecurityContextLogoutHandler());
    logoutFilter.setFilterProcessesUrl(casServiceLogout);
    return logoutFilter;
}

WebSecurityCasConfig配置:

    @Autowired
CasAuthenticationProvider casAuthenticationProvider;

@Autowired
CasAuthenticationEntryPoint casAuthenticationEntryPoint;

@Autowired
LogoutFilter requestSingleLogoutFilter;

@Autowired
ServiceProperties serviceProperties;
    
public CasAuthenticationFilter casAuthenticationFilter() throws Exception{
    CasAuthenticationFilter casAuthenticationFilter=new CasAuthenticationFilter();
    casAuthenticationFilter.setAuthenticationManager(authenticationManager());
    casAuthenticationFilter.setServiceProperties(serviceProperties);
    casAuthenticationFilter.setProxyGrantingTicketStorage(CasConfing.pgtStorage);
    casAuthenticationFilter.setProxyReceptorUrl(CasConfing.casServiceProxyCallbackUrl);
    casAuthenticationFilter.setAuthenticationDetailsSource(new ServiceAuthenticationDetailsSource(serviceProperties));
    casAuthenticationFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(CasConfing.casServiceFailureHandler));

    return casAuthenticationFilter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    // TODO Auto-generated method stub
    http
    .authorizeRequests()
    .antMatchers("/cas/casfailed").permitAll()
    .antMatchers("/secure/extreme/").access("hasRole('ROLE_SUPERVISOR')")
    .antMatchers("/secure/**").access("hasRole('ROLE_USER')")
    .anyRequest().authenticated()
    .and()
    .logout()
    .logoutUrl("/logout/cas")
    .logoutSuccessUrl(CasConfing.casServerUrlLogout+"?service="+CasConfing.casServiceHost+"/index")
    .permitAll()
    .and()
    .csrf().disable();

    //CAS服務器的單點登錄
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setCasServerUrlPrefix(CasConfing.casServerUrlPrefix);
    http
    .exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint)
    .and()
    .addFilter(casAuthenticationFilter())
    .addFilterBefore(requestSingleLogoutFilter, LogoutFilter.class)
    .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class);
}


@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // TODO Auto-generated method stub
    auth.authenticationProvider(casAuthenticationProvider);
    super.configure(auth);
}

CustomCasUserDetailsService自定義認證用戶信息處理配置:

@Service

public class CustomCasUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken>{

@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
    // TODO Auto-generated method stub
    System.err.println("當前認證成功的用戶名:"+token.getName());
    List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
    GrantedAuthority grantedAuthority=new SimpleGrantedAuthority("ROLE_SUPERVISOR");
    grantedAuthorities.add(grantedAuthority);
    grantedAuthority=new SimpleGrantedAuthority("ROLE_USER");
    grantedAuthorities.add(grantedAuthority);
    
    return new User(token.getName(), "a52302c58f4a60f49b1ad2f36add6d0a-000000", grantedAuthorities);
}

}

至此,Spring Security+CAS集成配置以完成。

您可以通過訪問客戶端Security安全頁面:
http://127.0.0.1:8080/index,
security會轉到CAS服務器登錄鏈接
http://127.0.0.1:8081/cas/login?service=http%3A%2F%2F127.0.0.1%3A8080%2Flogin%2Fcas
登錄認證通過后即可訪問安全頁面。

多站點:
分別部署兩個站點:serviceCas01,serviceCas02
 http://127.0.0.1:8080/serviceCas01/index,
 http://127.0.0.1:8082/serviceCas02/index,
serviceCas01登錄認證成功后,直接通過訪問  http://127.0.0.1:8082/serviceCas02/index,即可無需登錄訪問。通過http://127.0.0.1:8080/serviceCas01/logout/cas成功登出后,  重新刷新頁面http://127.0.0.1:8082/serviceCas02/index,也會登出。

后續(xù)有時間,再配圖啦。不足之處,謝謝指教。

銘言:  
        吾等前方,再無對手
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. CAS 簡介 1.1. What is CAS ? CAS ( Central Authenti...
    人在碼途閱讀 9,827評論 3 51
  • 簡介 Cas介紹 CAS ( Central Authentication Service ),最初由耶魯大學的S...
    但莫閱讀 14,728評論 0 10
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,948評論 18 139
  • 以下是官網直譯:https://oauth.net/ 1. 首頁 OAuth是一種開放協(xié)議(注:協(xié)議是公開的,任何...
    JacoChan閱讀 11,355評論 0 20
  • [00:10.00]Lyrics by @MPlover [00:22.45]connection [00:24....
    猴大貓閱讀 211評論 0 0