一、企業(yè)單一登錄(CAS)
1.Java(Spring Webflow / MVC servlet)服務(wù)器組件
2.可插拔認(rèn)證支持(LDAP,數(shù)據(jù)庫(kù)丑掺,X.509,雙因素)
3.支持多種協(xié)議(CAS述雾,SAML街州,OAuth,OpenID)
4.跨平臺(tái)的客戶(hù)端支持(Java玻孟,.Net唆缴,PHP,Perl黍翎,Apache等)
5.與uPortal面徽,Liferay,BlueSocket,Moodle和Google Apps集成趟紊,僅舉幾例
CAS提供了一個(gè)友好的開(kāi)源社區(qū)氮双,積極支持和貢獻(xiàn)項(xiàng)目。雖然該項(xiàng)目植根于更高級(jí)的開(kāi)放源代碼霎匈,但已經(jīng)發(fā)展成為世界500強(qiáng)企業(yè)和小型專(zhuān)用設(shè)施的國(guó)際用戶(hù)戴差。
二、如何部署您的CAS
在項(xiàng)目中安裝CAS服務(wù)器铛嘱,需要去官方github下載CAS標(biāo)準(zhǔn)WAR文件暖释,在WAR文件中有標(biāo)準(zhǔn)的單點(diǎn)登錄登出頁(yè)面。當(dāng)然您還需要對(duì)deployerConfigContext.xml中指定AuthenticationHandler進(jìn)行簡(jiǎn)單的修改墨吓,已滿(mǎn)足您對(duì)數(shù)據(jù)庫(kù)的操作需求球匕。CAS 本身包含大量的AuthenticationHandler,可以協(xié)助解決相應(yīng)的問(wèn)題帖烘。
除CAS服務(wù)器本身之外亮曹,其他關(guān)鍵角色當(dāng)然是在企業(yè)中部署的安全Web應(yīng)用程序,這些Web應(yīng)用程序被稱(chēng)為“服務(wù)“蚓让。有三種類(lèi)型的服務(wù):驗(yàn)證服務(wù)票據(jù)乾忱,獲得代理票據(jù),驗(yàn)證代理票據(jù)历极。驗(yàn)證代理票據(jù)的不同之處在于代理列表必須經(jīng)過(guò)驗(yàn)證,并且通持缘瑁可以重用代理趟卸。
CAS本身設(shè)計(jì)在HTTPS環(huán)境下,在本地測(cè)試以及個(gè)人學(xué)習(xí)情況下可以對(duì)CAS做些相應(yīng)修改氏义,使它支持HTTP訪問(wèn)锄列。在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能夠通過(guò)數(shù)據(jù)庫(kù)鑒定用戶(hù)憑證克婶,需要配置Database Authentication筒严。官方文檔詳見(jiàn):https://apereo.github.io/cas/4.2.x/installation/Database-Authentication.html。數(shù)據(jù)庫(kù)認(rèn)證有四種:
1.QueryDatabaseAuthenticationHandler情萤,通過(guò)用戶(hù)名和明文密碼進(jìn)行驗(yàn)證鸭蛙。
首先在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,通過(guò)查詢(xún)用戶(hù)名和密碼來(lái)搜索用戶(hù)記錄; 如果至少有一個(gè)結(jié)果被發(fā)現(xiàn)筋岛,用戶(hù)將被認(rè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,嘗試使用用戶(hù)名和(散列)密碼創(chuàng)建數(shù)據(jù)庫(kù)連接來(lái)對(duì)用戶(hù)進(jìn)行身份驗(yàn)證。
在deployerConfigContext.xml中配置
<alias name="bindModeSearchDatabaseAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="dataSource" alias="bindSearchDatabaseDataSource" />
4.QueryAndEncodeDatabaseAuthenticationHandler肪获,一個(gè)JDBC查詢(xún)處理程序寝凌,它將撤回用戶(hù)的密碼和私有salt值,并使用公共salt值驗(yàn)證編碼的密碼孝赫。 假設(shè)一切都在同一個(gè)數(shù)據(jù)庫(kù)表內(nèi)硫兰。 支持迭代次數(shù)和私鹽的設(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ù)認(rèn)證方式寒锚,修改完成后丟到tomcat下運(yùn)行即可劫映。
cas的訪問(wèn)地址:ip:port/cas/login
cas的登出地址:ip:port/cas/logout
三、Spring Security和CAS的集成
Web瀏覽器刹前,CAS服務(wù)器和Spring安全服務(wù)之間的基本交互如下:
CAS或Spring Security不管理公共頁(yè)面的處理泳赋,當(dāng)用戶(hù)請(qǐng)求一個(gè)安全的頁(yè)面或者它使用的一個(gè)安全的頁(yè)面。 Spring Security的ExceptionTranslationFilter將檢測(cè)到AccessDeniedException或AuthenticationException喇喉。
由于用戶(hù)的Authentication對(duì)象(或缺少)導(dǎo)致AuthenticationException祖今,因此ExceptionTranslationFilter將調(diào)用已配置的AuthenticationEntryPoint。如果使用CAS拣技,這將是CasAuthenticationEntryPoint類(lèi)千诬。
CasAuthenticationEntryPoint將把用戶(hù)的瀏覽器重定向到CAS服務(wù)器。它還會(huì)顯示一個(gè)服務(wù)參數(shù)膏斤,它是Spring Security服務(wù)(您的應(yīng)用程序)的回調(diào)URL徐绑。例如,瀏覽器重定向到的URL可能是
https://my.company.com/cas/login莫辨?service= HTTPS%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin / CAS傲茄。
用戶(hù)的瀏覽器重定向到CAS后液走,系統(tǒng)會(huì)提示用戶(hù)輸入用戶(hù)名和密碼坚冀。如果用戶(hù)提交了一個(gè)表示他們以前登錄過(guò)的會(huì)話cookie炕桨,他們將不會(huì)再被提示重新登錄(這個(gè)過(guò)程是個(gè)例外儡循,我們將在后面介紹)凑保。 CAS將使用上述的PasswordHandler(或使用CAS 3.0的AuthenticationHandler)來(lái)決定用戶(hù)名和密碼是否有效姐仅。
CAS成功登錄后唯卖,會(huì)將用戶(hù)瀏覽器重定向到原始服務(wù)眠寿。它還將包含一個(gè)票據(jù)參數(shù)型酥,這是一個(gè)不透明的字符串山憨,代表“服務(wù)票據(jù)”。繼續(xù)前面的例子冕末,瀏覽器被重定向到的URL可能是
https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ萍歉。
回到服務(wù)Web應(yīng)用程序,CasAuthenticationFilter總是監(jiān)聽(tīng)/ login / cas的請(qǐng)求(這是可配置的档桃,但是我們將使用這個(gè)介紹中的默認(rèn)值)枪孩。處理過(guò)濾器將構(gòu)建代表服務(wù)票據(jù)的UsernamePasswordAuthenticationToken。主體將等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而憑證將是服務(wù)票證不透明值蔑舞。這個(gè)認(rèn)證請(qǐng)求將被交給配置的AuthenticationManager拒担。
AuthenticationManager實(shí)現(xiàn)將是ProviderManager,它又被配置了CasAuthenticationProvider攻询。 CasAuthenticationProvider只響應(yīng)包含CAS特定主體(如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和CasAuthenticationToken(稍后討論)的UsernamePasswordAuthenticationToken从撼。
CasAuthenticationProvider將使用TicketValidator實(shí)現(xiàn)來(lái)驗(yàn)證服務(wù)票證。這通常是一個(gè)Cas20ServiceTicketValidator钧栖,它是包含在CAS客戶(hù)端庫(kù)中的一個(gè)類(lèi)低零。如果應(yīng)用程序需要驗(yàn)證代理票證,則使用Cas20ProxyTicketValidator拯杠。 TicketValidator向CAS服務(wù)器發(fā)出HTTPS請(qǐng)求掏婶,以驗(yàn)證服務(wù)票據(jù)。它也可能包含一個(gè)代理回調(diào)URL潭陪,它包含在這個(gè)例子中:
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服務(wù)器,驗(yàn)證請(qǐng)求將被接收依溯。如果所提供的服務(wù)票據(jù)與發(fā)行票據(jù)的服務(wù)URL相匹配老厌,則CAS將以XML表示用戶(hù)名的肯定響應(yīng)。如果任何代理參與了身份驗(yàn)證(如下所述)黎炉,那么代理列表也會(huì)包含在XML響應(yīng)中枝秤。
[可選]如果對(duì)CAS驗(yàn)證服務(wù)的請(qǐng)求包含代理回調(diào)URL(在pgtUrl參數(shù)中),則CAS將在XML響應(yīng)中包含一個(gè)pgtIou字符串拜隧。這pgtIou代表代理授予票借條宿百。然后,CAS服務(wù)器將創(chuàng)建自己的HTTPS連接回pgtUrl洪添。這是為了相互認(rèn)證CAS服務(wù)器和聲稱(chēng)的服務(wù)URL。 HTTPS連接將用于將授權(quán)票據(jù)的代理發(fā)送到原始Web應(yīng)用程序雀费。例如干奢,
https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH。
Cas20TicketValidator將解析從CAS服務(wù)器收到的XML盏袄。它將返回CasAuthenticationProvider TicketResponse忿峻,其中包括用戶(hù)名(強(qiáng)制),代理列表(如果有任何涉及)辕羽,和代理授予票證IOU(如果代理回調(diào)被請(qǐng)求)逛尚。
接下來(lái),CasAuthenticationProvider將調(diào)用已配置的CasProxyDecider刁愿。 CasProxyDecider指示TicketResponse中的代理列表是否可以被服務(wù)接受绰寞。 Spring Security提供了幾個(gè)實(shí)現(xiàn):RejectProxyTickets,AcceptAnyCasProxy和NamedCasProxyDecider。這些名稱(chēng)在很大程度上是不言而喻的滤钱,除了NamedCasProxyDecider允許提供可信代理列表觉壶。
CasAuthenticationProvider接下來(lái)將請(qǐng)求一個(gè)AuthenticationUserDetailsS??ervice來(lái)加載適用于Assertion中包含的用戶(hù)的GrantedAuthority對(duì)象。
如果沒(méi)有問(wèn)題件缸,CasAuthenticationProvider構(gòu)造一個(gè)CasAuthenticationToken铜靶,包括TicketResponse和GrantedAuthoritys中包含的細(xì)節(jié)。
控制然后返回到CasAuthenticationFilter他炊,它將創(chuàng)建的CasAuthenticationToken放置在安全上下文中争剿。
用戶(hù)的瀏覽器被重定向到導(dǎo)致AuthenticationException的原始頁(yè)面(或根據(jù)配置的自定義目標(biāo))。
四痊末、Spring Boot +Spring Security+CAS開(kāi)發(fā)(代理票據(jù)認(rèn)證)
CasAuthenticationProvider區(qū)分有狀態(tài)和無(wú)狀態(tài)客戶(hù)端蚕苇。 有狀態(tài)的客戶(hù)端被認(rèn)為是提交給CasAuthenticationFilter的filterProcessUrl的。 無(wú)狀態(tài)客戶(hù)端是指向除FilterProcessUrl以外的URL向CasAuthenticationFilter提交身份驗(yàn)證請(qǐng)求的任何客戶(hù)端舌胶。
由于遠(yuǎn)程協(xié)議無(wú)法在HttpSession的上下文中呈現(xiàn)捆蜀,因此不可能依賴(lài)于在請(qǐng)求之間的會(huì)話中存儲(chǔ)安全上下文的默認(rèn)實(shí)踐。 此外幔嫂,由于CAS服務(wù)器在TicketValidator驗(yàn)證之后使其無(wú)效辆它,因此在后續(xù)請(qǐng)求中顯示相同的代理票證將不起作用。
CasConfing配置:
//客戶(hù)端配置
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服務(wù)端配置
@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);//允許所有代理回調(diào)鏈接
ticketValidator.setProxyGrantingTicketStorage(pgtStorage);
authenticationProvider.setTicketValidator(ticketValidator);
authenticationProvider.setAuthenticationUserDetailsService(customCasUserDetailsService);
//無(wú)狀態(tài)緩存
EhCacheBasedTicketCache ticketCache=new EhCacheBasedTicketCache();
ticketCache.setCache(new Cache("casTickets", 50, true, false, 3600, 900));
authenticationProvider.setStatelessTicketCache(ticketCache);
return authenticationProvider;
}
//單點(diǎn)登出履恩,跳轉(zhuǎn)到客戶(hù)端的登出鏈接
@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服務(wù)器的單點(diǎn)登錄
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自定義認(rèn)證用戶(hù)信息處理配置:
@Service
public class CustomCasUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken>{
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
// TODO Auto-generated method stub
System.err.println("當(dāng)前認(rèn)證成功的用戶(hù)名:"+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集成配置以完成。
您可以通過(guò)訪問(wèn)客戶(hù)端Security安全頁(yè)面:
http://127.0.0.1:8080/index,
security會(huì)轉(zhuǎn)到CAS服務(wù)器登錄鏈接
http://127.0.0.1:8081/cas/login?service=http%3A%2F%2F127.0.0.1%3A8080%2Flogin%2Fcas
登錄認(rèn)證通過(guò)后即可訪問(wèn)安全頁(yè)面切心。
多站點(diǎn):
分別部署兩個(gè)站點(diǎn):serviceCas01,serviceCas02
http://127.0.0.1:8080/serviceCas01/index,
http://127.0.0.1:8082/serviceCas02/index,
serviceCas01登錄認(rèn)證成功后飒筑,直接通過(guò)訪問(wèn) http://127.0.0.1:8082/serviceCas02/index,即可無(wú)需登錄訪問(wèn)。通過(guò)http://127.0.0.1:8080/serviceCas01/logout/cas成功登出后绽昏, 重新刷新頁(yè)面http://127.0.0.1:8082/serviceCas02/index,也會(huì)登出协屡。
后續(xù)有時(shí)間,再配圖啦全谤。不足之處肤晓,謝謝指教。
銘言:
吾等前方认然,再無(wú)對(duì)手