使用Spring Security 集成 CAS 完成单点登录

一、企业单一登录(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接下来将请求一个AuthenticationUserDetailsS​​ervice来加载适用于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,也会登出。

后续有时间,再配图啦。不足之处,谢谢指教。

铭言:  
        吾等前方,再无对手

你可能感兴趣的:(使用Spring Security 集成 CAS 完成单点登录)