一、企业单一登录(CAS)
1.Java(Spring Webflow / MVC servlet)服务器组件
2.可插拔认证支持(LDAP,数据库,X.509,双因素)
3.支持多种协议(CAS,SAML,OAuth,OpenID)
4.跨平台的客户端支持(Java,.Net,PHP,Perl,Apache等)
5.与uPortal,Liferay,BlueSocket,Moodle和Google Apps集成,仅举几例
CAS提供了一个友好的开源社区,积极支持和贡献项目。虽然该项目植根于更高级的开放源代码,但已经发展成为世界500强企业和小型专用设施的国际用户。
二、如何部署您的CAS
在项目中安装CAS服务器,需要去官方github下载CAS标准WAR文件,在WAR文件中有标准的单点登录登出页面。当然您还需要对deployerConfigContext.xml中指定AuthenticationHandler进行简单的修改,已满足您对数据库的操作需求。CAS 本身包含大量的AuthenticationHandler,可以协助解决相应的问题。
除CAS服务器本身之外,其他关键角色当然是在企业中部署的安全Web应用程序,这些Web应用程序被称为“服务“。有三种类型的服务:验证服务票据,获得代理票据,验证代理票据。验证代理票据的不同之处在于代理列表必须经过验证,并且通常可以重用代理。
CAS本身设计在HTTPS环境下,在本地测试以及个人学习情况下可以对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文件中的:
为了让CAS能够通过数据库鉴定用户凭证,需要配置Database Authentication。官方文档详见:https://apereo.github.io/cas/4.2.x/installation/Database-Authentication.html。数据库认证有四种:
1.QueryDatabaseAuthenticationHandler,通过用户名和明文密码进行验证。
首先在cas.properties中配置:
# cas.jdbc.authn.query.sql=select password from users where username=?
在deployerConfigContext.xml中配置
2.SearchModeSearchDatabaseAuthenticationHandler,通过查询用户名和密码来搜索用户记录; 如果至少有一个结果被发现,用户将被认证。
首先在cas.properties中配置
# cas.jdbc.authn.search.password=
# cas.jdbc.authn.search.user=
# cas.jdbc.authn.search.table=
在deployerConfigContext.xml中配置
3.BindModeSearchDatabaseAuthenticationHandler,尝试使用用户名和(散列)密码创建数据库连接来对用户进行身份验证。
在deployerConfigContext.xml中配置
4.QueryAndEncodeDatabaseAuthenticationHandler,一个JDBC查询处理程序,它将撤回用户的密码和私有salt值,并使用公共salt值验证编码的密码。 假设一切都在同一个数据库表内。 支持迭代次数和私盐的设置。
首先在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中配置
一般选择第四种数据认证方式,修改完成后丢到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服务器。它还会显示一个服务参数,它是Spring Security服务(您的应用程序)的回调URL。例如,浏览器重定向到的URL可能是
https://my.company.com/cas/login?service= HTTPS%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin / CAS。
用户的浏览器重定向到CAS后,系统会提示用户输入用户名和密码。如果用户提交了一个表示他们以前登录过的会话cookie,他们将不会再被提示重新登录(这个过程是个例外,我们将在后面介绍)。 CAS将使用上述的PasswordHandler(或使用CAS 3.0的AuthenticationHandler)来决定用户名和密码是否有效。
CAS成功登录后,会将用户浏览器重定向到原始服务。它还将包含一个票据参数,这是一个不透明的字符串,代表“服务票据”。继续前面的例子,浏览器被重定向到的URL可能是
https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ。
回到服务Web应用程序,CasAuthenticationFilter总是监听/ login / cas的请求(这是可配置的,但是我们将使用这个介绍中的默认值)。处理过滤器将构建代表服务票据的UsernamePasswordAuthenticationToken。主体将等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而凭证将是服务票证不透明值。这个认证请求将被交给配置的AuthenticationManager。
AuthenticationManager实现将是ProviderManager,它又被配置了CasAuthenticationProvider。 CasAuthenticationProvider只响应包含CAS特定主体(如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和CasAuthenticationToken(稍后讨论)的UsernamePasswordAuthenticationToken。
CasAuthenticationProvider将使用TicketValidator实现来验证服务票证。这通常是一个Cas20ServiceTicketValidator,它是包含在CAS客户端库中的一个类。如果应用程序需要验证代理票证,则使用Cas20ProxyTicketValidator。 TicketValidator向CAS服务器发出HTTPS请求,以验证服务票据。它也可能包含一个代理回调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服务器,验证请求将被接收。如果所提供的服务票据与发行票据的服务URL相匹配,则CAS将以XML表示用户名的肯定响应。如果任何代理参与了身份验证(如下所述),那么代理列表也会包含在XML响应中。
[可选]如果对CAS验证服务的请求包含代理回调URL(在pgtUrl参数中),则CAS将在XML响应中包含一个pgtIou字符串。这pgtIou代表代理授予票借条。然后,CAS服务器将创建自己的HTTPS连接回pgtUrl。这是为了相互认证CAS服务器和声称的服务URL。 HTTPS连接将用于将授权票据的代理发送到原始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提供了几个实现:RejectProxyTickets,AcceptAnyCasProxy和NamedCasProxyDecider。这些名称在很大程度上是不言而喻的,除了NamedCasProxyDecider允许提供可信代理列表。
CasAuthenticationProvider接下来将请求一个AuthenticationUserDetailsService来加载适用于Assertion中包含的用户的GrantedAuthority对象。
如果没有问题,CasAuthenticationProvider构造一个CasAuthenticationToken,包括TicketResponse和GrantedAuthoritys中包含的细节。
控制然后返回到CasAuthenticationFilter,它将创建的CasAuthenticationToken放置在安全上下文中。
用户的浏览器被重定向到导致AuthenticationException的原始页面(或根据配置的自定义目标)。
四、Spring Boot +Spring Security+CAS开发(代理票据认证)
CasAuthenticationProvider区分有状态和无状态客户端。 有状态的客户端被认为是提交给CasAuthenticationFilter的filterProcessUrl的。 无状态客户端是指向除FilterProcessUrl以外的URL向CasAuthenticationFilter提交身份验证请求的任何客户端。
由于远程协议无法在HttpSession的上下文中呈现,因此不可能依赖于在请求之间的会话中存储安全上下文的默认实践。 此外,由于CAS服务器在TicketValidator验证之后使其无效,因此在后续请求中显示相同的代理票证将不起作用。
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);
//无状态缓存
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
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
// TODO Auto-generated method stub
System.err.println("当前认证成功的用户名:"+token.getName());
List 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,也会登出。
后续有时间,再配图啦。不足之处,谢谢指教。
铭言:
吾等前方,再无对手