Versiones comparadas

Clave

  • Se ha añadido esta línea.
  • Se ha eliminado esta línea.
  • El formato se ha cambiado.

Tabla de contenidos

...

Login con CAS: cliente CAS en Spring Boot

Si queremos poner el login con el CAS, sin utilizar el portal de servicios, tendremos que hacer todo lo que se describe a continuación, si no sólo tendremos que añadir el secret y el tiempo en el que expira el token a nuestro application.properties, y pasar al apartado de "Cómo comprobar el token".

Esto es una versión inicial, todavía no tiene suplantación, se irá añadiendo conforme vayamos teniendo tiempo para hacerlo.

Tablas de base de datos

Para cada aplicación, necesitaremos crearnos varias tablas de base de datos. En primer lugar, necesitaremos una tabla para mantener la sesión, es decir, para tener un registro de los tokens que se han dado, y poder comprobar si han expirado o no. Esta tabla podría llamarse CAS_TOKEN_SESSION, y podemos crearla con este script, cambiando el esquema (que en este caso es PORTALFUNDEWEB):

...

Después, seguimos el diagrama que se muestra en esta página de la wiki de FundeWeb 2.0. Se muestra tanto el esquema como el script para crear las tablas. Se tienen tres tablas, Usuarios, Roles y Objetivos, con tablas intermedias entre ellas. Lo más habitual es que se utilicen los Usuarios y los Roles, los Objetivos son para dar permisos más detallados, para ciertas partes de la aplicación. Vamos a ver las entidades correspondientes a estas tablas.

Cosas a añadir en el proyecto

Dependencias en el pom.xml

Tendremos que añadir estas dependencias al pom.xml:

Bloque de código
languagexml
        <!-- CAS -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-cas</artifactId>
		</dependency>

		<!-- JWT -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>

Parámetros en application.properties

En nuestro application.properties, además de tener definido el datasource de la aplicación, tenemos que añadir las siguientes propiedades:

...

¡Ojo! Estas propiedades están probadas en local, hay que ver lo que hay que cambiar en los demás entornos. También, el secret deberá ser el correspondiente, lo he omitido por seguridad.

Configuración en la clase inicial

En la calse que inicia nuestra aplicación, que se llamará NombreProyectoApplication.java, tendremos que añadir varias cosas, dejándola como en este ejemplo, pero con el método main que nos venía:

Bloque de código
languagejava
package es.um.atica.----;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSessionEvent;

import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.client.RestTemplate;

import es.um.atica.----.cas.services.UsuarioService;

@SpringBootApplication
public class LogincasApplication {

	@Value( "${login.config.serviceurl}" )
	private String serviceurl;

	@Value( "${login.config.sso.loginurl}" )
	private String ssoLoginUrl;

	@Value( "${login.config.sso.logouturl}" )
	private String ssoLogoutUrl;

	@Value( "${login.config.sso.serverurl}" )
	private String ssoServerUrl;

	public static void main( String[] args ) {
		SpringApplication.run( LogincasApplication.class, args );
	}

	@Bean
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

	@Bean
	public ServiceProperties serviceProperties() {
		final ServiceProperties serviceProperties = new ServiceProperties();
		serviceProperties.setSendRenew( false );
		serviceProperties.setService( serviceurl );
		serviceProperties.setAuthenticateAllArtifacts( true );
		return serviceProperties;
	}

	@Bean
	@Primary
	public AuthenticationEntryPoint authenticationEntryPoint( ServiceProperties sP ) {
		final CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint() {

			@Override
			protected String createServiceUrl( final HttpServletRequest request, final HttpServletResponse response ) {
				String serviceUrl = serviceProperties().getService();
				final String callback = request.getParameter( "callback" );
				if ( callback != null ) {
					serviceUrl += "/" + callback;
				}
				return serviceUrl;
			}
		};
		entryPoint.setLoginUrl( ssoLoginUrl );
		entryPoint.setServiceProperties( sP );
		return entryPoint;
	}

	@Bean
	public TicketValidator ticketValidator() {
		return new Cas30ServiceTicketValidator( ssoServerUrl );
	}

	@Bean
	public UserDetailsService generateUserDetailsService() {
		return new UsuarioService();
	}

	@Bean
	public CasAuthenticationProvider casAuthenticationProvider() {
		final CasAuthenticationProvider provider = new CasAuthenticationProvider();
		provider.setServiceProperties( serviceProperties() );
		provider.setTicketValidator( ticketValidator() );
		provider.setUserDetailsService( generateUserDetailsService() );
		provider.setKey( "CAS_PROVIDER_P15S" );
		return provider;
	}

	@Bean
	public SecurityContextLogoutHandler securityContextLogoutHandler() {
		return new SecurityContextLogoutHandler();
	}

	@Bean
	public LogoutFilter logoutFilter() {
		final LogoutFilter logoutFilter = new LogoutFilter( ssoLogoutUrl, securityContextLogoutHandler() );
		logoutFilter.setFilterProcessesUrl( "/logout/cas" );
		return logoutFilter;
	}

	@Bean
	public SingleSignOutFilter singleSignOutFilter() {
		final SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
		// singleSignOutFilter.setCasServerUrlPrefix( ssoServerUrl );
		singleSignOutFilter.setIgnoreInitConfiguration( true );
		return singleSignOutFilter;
	}

	@EventListener
	public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener( HttpSessionEvent event ) {
		return new SingleSignOutHttpSessionListener();
	}
}

SecurityConfig

Nuestra clase SecurityConfig quedará así:

Bloque de código
languagejava
package es.um.atica.----.cas.config;

import static es.um.atica.---.Constants.SESSION_TOKEN_HEADER;

import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;

import org.jasig.cas.client.session.SingleSignOutFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import es.um.atica.----.cas.handlers.CustomizedLogoutHandler;
import es.um.atica.----.cas.handlers.CustomizedUrlAuthenticationSuccessHandler;

@EnableWebSecurity
@EnableGlobalMethodSecurity( securedEnabled = true )
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	private final AuthenticationProvider authenticationProvider;
	private final LogoutFilter logoutFilter;
	private final SingleSignOutFilter singleSignOutFilter;
	private final AuthenticationEntryPoint authenticationEntryPoint;
	private final ServiceProperties serviceProperties;

	@Autowired
	public SecurityConfig ( AuthenticationEntryPoint aep, LogoutFilter lF, SingleSignOutFilter ssF,
			CasAuthenticationProvider casAuthenticationProvider, ServiceProperties sp ) {
		authenticationProvider = casAuthenticationProvider;
		logoutFilter = lF;
		singleSignOutFilter = ssF;
		authenticationEntryPoint = aep;
		serviceProperties = sp;
	}

	// Autenticacion básica con usuario autogenerado
	@Override
	protected void configure( HttpSecurity http ) throws Exception {

		http.cors().and().csrf().disable().addFilter( casAuthenticationFilter( serviceProperties ) ).authorizeRequests()
		.regexMatchers( "^/entrada(\\/)?(\\?.+)?$" ).authenticated().and().authorizeRequests()
		.regexMatchers( "/public/*", "/private/*" ).permitAll().and().httpBasic()
		.authenticationEntryPoint( authenticationEntryPoint ).and().logout()
		.logoutSuccessHandler( customizedLogoutHandler() ).deleteCookies( SESSION_TOKEN_HEADER ).and()
		.addFilterBefore( singleSignOutFilter, CasAuthenticationFilter.class )
		.addFilterBefore( logoutFilter, LogoutFilter.class );
	}

	@Bean
	CorsConfigurationSource corsConfigurationSource() {
		final CorsConfiguration configuration = new CorsConfiguration();
		configuration.setAllowedOriginPatterns( Arrays.asList( "*" ) );
		configuration.setAllowedMethods( Arrays.asList( "*" ) );
		configuration.setAllowedHeaders( Arrays.asList( "*" ) );
		configuration.setAllowCredentials( true );
		final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration( "/**", configuration );
		return source;
	}

	@Bean
	public CustomizedLogoutHandler customizedLogoutHandler() {
		return new CustomizedLogoutHandler();
	}

	@Override
	protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
		auth.authenticationProvider( authenticationProvider );
	}

	@Override
	protected AuthenticationManager authenticationManager() throws Exception {
		return new ProviderManager( Arrays.asList( authenticationProvider ) );
	}

	@Bean
	AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails> dynamicServiceResolver() {
		return ( HttpServletRequest context ) -> {
			final String url = context.getRequestURL().toString();
			return new ServiceAuthenticationDetails() {

				private static final long serialVersionUID = 1L;

				@Override
				public String getServiceUrl() {
					return url;
				}
			};
		};
	}

	@Bean
	public CasAuthenticationFilter casAuthenticationFilter( ServiceProperties sP ) throws Exception {
		final CasAuthenticationFilter filter = new CasAuthenticationFilter();
		filter.setServiceProperties( sP );
		filter.setAuthenticationManager( authenticationManager() );
		filter.setFilterProcessesUrl( "/entrada/*" );
		filter.setAuthenticationDetailsSource( dynamicServiceResolver() );
		filter.setAuthenticationSuccessHandler( new CustomizedUrlAuthenticationSuccessHandler() );
		return filter;
	}

}

Constantes

Se utilizan una serie de constantes que nos definimos en un archivo Constans.java situado en el paquete raíz, el mismo que la clase que inicia la aplicación, y contendrá, al menos, lo siguiente (podemos añadir constantes que nos hagan falta para otras cosas del proyecto):

Bloque de código
languagejava
package es.um.atica.----;


public final class Constants {

	private Constants() {}

	public static final String IS_ADMIN = "isAdmin";

	public static final String USER_PARAM = "user";

	public static final String SLASH = "/";

	public static final String DEFAULT_CALLBACK = "/";

	public static final String LOCAL_ENV = "local";

	public static final String DEV_ENV = "desarrollo";

	public static final String TEST_ENV = "test";

	public static final String APIUM_DEV_ENV = "DESA";

	public static final String APIUM_TEST_ENV = "TEST";

	public static final String PUBLIC_PREFIX = "/public";

	public static final String PRIVATE_PREFIX = "/private";

	public static final String ROLE_ADMIN = "ROLE_ADMIN";

	public static final String ROLE_ANONYMOUS = "ROLE_ANONYMOUS";

	public static final String ROLE_USER = "ROLE_USER";

	public static final String AUTHORIZATION_HEADER = "Authorization";

	public static final String CLAIMS_PARAM = "claims";

	public static final String ROLENAME_PARAM = "roleName";

	public static final String IMPERSONATOR_PARAM = "impersonator";

	public static final String SESSION_TOKEN_HEADER = "refresh-token";

	public static final String EMAIL_PARAM = "email";

	public static final String USER_AGENT_HEADER = "User-Agent";

	public static final String LANGUAGE_PARAM = "inLanguage";

	public static final String REFERER_HEADER = "referer";

	public static final String CAS_ATTR_LDAP_GROUPS = "ldapGroups";

	public static final String CAS_ATTR_PLACE = "place";

	public static final String CAS_ATTR_GENDER = "gender";

}

Resto de clases

El resto de clases se pueden descargar aquí. Se incluyen las entidades correspondientes a base de datos, y sus correspondientes repositorios. La estructura de paquetes del proyecto, incluyendo el cliente cas, quedaría así:

...

Podemos descargarlas y meterlas en nuestro proyecto, cambiando los nombres de los paquetes para que coincidan con los de nuestro proyecto.

Cómo comprobar el token

Para recibir el token en nuestro método REST, lo haremos en el header auth-token de la petición, y lo especificaremos en nuestros métodos Java con @RequestHeader, y podemos comprobarlo de la siguiente manera:

...