Ecco un pratico esempio di come integrare Apereo CAS con una o più applicazioni web scritte in Java che utilizzano Spring Security. Il risultato è un web Single-Sign On centralizzato che vede CAS come unico sistema di controllo degli accessi e che consente perciò agli utenti di poter fruire di più risorse protette, effettuando un’unica autenticazione. In questo esempio, la comunicazione tra il CAS server e le applicazioni web protette, anche chiamate CAS client o CAS service, è ticket-based ed è implementata su protocollo CAS 3.0 (altri protocolli supportati da CAS sono SAML, OpenID, OAuth). L’immagine seguente mostra una visione ad alto livello dell’architettura del sistema.

Stack
- Apereo CAS 5.2.3
- Spring Security 5.0.3.RELEASE
- Spring MVC 5.0.3.RELEASE
- LDAP Active Directory 2012
- JDK 1.8.0_112
- Maven 3.5.0
NOTE: Il codice sorgente trattato in questo articolo si trova nella directory mvc-security-cas
CAS Server Setup
Scaricare il sorgente di CAS server dal repository git e abilitare un handler di autenticazione. In questo esempio ho integrato CAS a un server Active Directory con degli utenti di test, per cui è sufficiente aggiungere la dipendenza al supporto LDAP nel pom.xml del sorgente.
$ git clone https://github.com/apereo/cas-overlay-template.git
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-ldap</artifactId>
<version>${cas.version}</version>
</dependency>
Per definire le webapp che sono protette da CAS utilizzerò un file JSON ma è necessario anche in questo caso aggiungere nel pom.xml la dipendenza a json-service-registry per la definizione dei servizi in formato json
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-json-service-registry</artifactId>
<version>${cas.version}</version>
</dependency>
Il nome del file json che definisce i servizi deve essere creato secondo la convenzione segueente:
## ./cas-server/src/main/cas-server-config/client-services/appA-100.json
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "http://localhost:9090/appA.*",
"name" : "appA",
"id" : 100,
"evaluationOrder" : 1
}
## ./cas-server/src/main/cas-server-config/client-services/appB-200.json
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "http://localhost:9090/appB.*",
"name" : "appB",
"id" : 200,
"evaluationOrder" : 1
}
Assicurarsi che CAS sia in esecuzione su HTTPS altrimenti la funzionalità di SSO non funzionerà. La pagina di login di CAS mostra un warning di questo tipo:
In order to have SSO on work, you must log in over HTTPS.
## src/main/cas-server-config/cas.properties
## CAS on HTTPS
##
server.context-path=/cas
cas.server.name=https://localhost:8443
cas.server.prefix=https://localhost:8443/cas
# These properties have impacts only on the embedded Tomcat server
server.ssl.enabled=true
server.port=8443
server.ssl.key-store=file:/home/user/cas-certs/cas.keystore
server.ssl.key-store-password=store12345
server.ssl.key-password=key12345
## SERVICES REGISTRY
##
cas.serviceRegistry.initFromJson=false
cas.serviceRegistry.json.location=file:/home/user/project/src/main/cas-server-config/client-services
## LDAP AUTHENTICATION
##
cas.authn.ldap[0].type=AD
cas.authn.ldap[0].ldapUrl=ldap://192.168.56.120:389
cas.authn.ldap[0].useSsl=false
cas.authn.ldap[0].baseDn=OU=test-foo,DC=example,DC=com
cas.authn.ldap[0].bindDn=CN=Administrator,CN=Users,DC=example,DC=com
cas.authn.ldap[0].bindCredential=12345
cas.authn.ldap[0].userFilter=sAMAccountName={user}
cas.authn.ldap[0].dnFormat=%s@example.com
cas.authn.ldap[0].principalAttributeId=sAMAccountName
CAS Client Setup
La webapp di questo esempio utilizza Spring MVC e Spring Security che fornisce nativamente supporto all’integrazione con CAS. Basta aggiungere la dipendenza dal modulo nel pom.xml.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
Per inizializzare l’application context, utilizzo un’implementazione di Spring WebApplicationInitializer. Questo mi consente di istanziare programmaticamente filtri, servlet, e listener direttamente in Java senza utilizzare il tradizionale approccio via web.xml.
// Creating the servlet application context by using annotation based context
AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
webApplicationContext.register(MvcConfigurer.class);
webApplicationContext.register(CasConfigurer.class);
webApplicationContext.register(SecurityConfigurer.class);
webApplicationContext.setServletContext(servletContext);
// Spring DelegatingFilterProxy which allows you to enable Spring Security and use your custom Filters
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("giusWebappFilterDelegator", new DelegatingFilterProxy("springSecurityFilterChain"));
filterRegistration.addMappingForUrlPatterns(null, false, "/*");
La definizione di tutti gli oggetti legati a CAS viene fatta via annotation. Lo scopo della classe CasConfigurer è proprio quello di centralizzare tutte le configurazioni di CAS in un’unico oggetto.
/**
* CAS global properties.
* @return
*/
@Bean
public ServiceProperties serviceProperties() {
String appLogin = "http://localhost:18080/mvc-casclient/login-cas";
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(appLogin);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/**
* The entry point of Spring Security authentication process (based on CAS).
* The user's browser will be redirected to the CAS login page.
* @return
*/
@Bean
public AuthenticationEntryPoint casAuthenticationEntryPoint() {
String casLogin = "https://localhost:8443/cas/login";
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(casLogin);
entryPoint.setServiceProperties(serviceProperties());
return entryPoint;
}
/**
* CAS ticket validator, if you plan to use CAS 3.0 protocol
* @return
*/
@Bean
public Cas30ServiceTicketValidator ticketValidatorCas30() {
Cas30ServiceTicketValidator ticketValidator = new Cas30ServiceTicketValidator("http://localhost:8080/cas");
return ticketValidator;
}
/**
* The authentication provider that integrates with CAS.
* This implementation uses CAS 3.0 protocol for ticket validation.
*
*/
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidatorCas30());
// Loads only a default set of authorities for any authenticated users (username and password are)
provider.setUserDetailsService((UserDetailsService) fakeUserDetailsService());
provider.setKey("CAS_PROVIDER_KEY_LOCALHOST");
return provider;
}
/**
* CAS Authentication Provider does not use credentials specified here for authentication. It only loads
* the authorities for a user, once they have been authenticated by CAS.
*
*/
@Bean
public User fakeUserDetailsService(){
return new User("fakeUser", "fakePass", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER"));
}
SecurityConfigurer estende Spring WebSecurityConfigurerAdapter e inizializza Spring Security.
// Let Spring resolves and injects collaborating beans into this class by @Autowired annotations...
@Autowired
private AuthenticationProvider casAuthenticationProvider;
@Autowired
private AuthenticationEntryPoint casAuthenticationEntryPoint;
@Autowired
private ServiceProperties casServiceProperties;
/**
* Configures web based security for specific http requests.
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/**").permitAll()
.anyRequest().authenticated();
http.httpBasic()
.authenticationEntryPoint(casAuthenticationEntryPoint);
http.addFilter(casAuthenticationFilter());
}
/**
* Configures multiple Authentication providers.
* AuthenticationManagerBuilder allows for easily building multiple authentication mechanisms in the order they're declared.
* CasAuthenticationProvider is used here.
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider);
}
/**
* Cas authentication filter responsible processing a CAS service ticket.
* Here, I was unable to declare this bean in the Cas configurator class( https://tinyurl.com/y9fzgma9 )
* @return
* @throws Exception
*/
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(casServiceProperties);
filter.setAuthenticationManager(authenticationManager());
return filter;
}
Il controller gestisce le url per le richieste verso la pagina di login dell’applicazione “/sso-login” e il redirect alla root web context “/”
@GetMapping("/sso-login")
public String login(HttpServletRequest request) {
return "redirect:/";
}
@RequestMapping(value = {"/"}, method = RequestMethod.GET)
public ModelAndView defaultView (HttpServletRequest request, HttpServletResponse response) {
String pageName = "index.html";
ModelAndView view = new ModelAndView(pageName);
return view;
}
Italiano
Inglese
