Estás viendo una versión antigua de esta página. Ve a la versión actual.

Comparar con el actual Ver el historial de la página

« Anterior Versión 46 Siguiente »


¿Qué login debo incluir?

Con el Portal de Servicios, tenemos dos vías para implementar el login, utilizar el propio login del Portal o implementar un cliente CAS para nuestra aplicación, y dependerá de dónde se sitúe esta.

  • Se utilizará el login del portal si la aplicación está totalmente integrada, es decir, si va a ser una aplicación que esté bajo micampus. 
  • Si es una aplicación independiente, habrá que implementar el cliente CAS en nuestro backend.

En ambos casos, ya que se usa siempre el CAS como sistema de autenticación (o bien la propia aplicación o bien el Portal de Servicios) deberemos asegurarnos que nuestra aplicación está dada de alta siguiendo la guía Alta de aplicaciones en CAS.

A continuación tenemos lo que hay que añadir a nuestro proyecto tanto con login en el Portal, como con login CAS.

Login con el Portal de Servicios

En nuestro backend, cuando usemos el login a través del Portal de Servicios, sólo necesitaremos poder comprobar el token. Para ello, necesitaremos incluir el secret en el archivo application.properties de nuestro proyecto:

es.um.jwt.secret=---

El valor del secret variará en cada entorno, pero para poder probarlo en local, tendremos que incluir el secret del entorno correspondiente con el que estemos haciendo las pruebas.

Una vez hecho esto, ya solo nos queda comprobar el token en los métodos privados de nuestro servicio REST, que está explicado en el apartado Cómo comprobar el token.

Login con CAS: cliente CAS en Spring Boot

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):

Query para crear tabla "CAS_TOKEN_SESSION"
  CREATE TABLE "PORTALFUNDEWEB"."CAS_TOKEN_SESSION" 
   (	"UUID" VARCHAR2(40 BYTE) NOT NULL ENABLE, 
	"LOGIN" VARCHAR2(60 BYTE) NOT NULL ENABLE, 
	"START_TIME" TIMESTAMP (6) NOT NULL ENABLE, 
	"LAST_REFRESH" TIMESTAMP (6) NOT NULL ENABLE, 
	"USER_AGENT" VARCHAR2(256 BYTE), 
	"PLACE" VARCHAR2(256 BYTE), 
	"LDAP_GROUPS" VARCHAR2(256 BYTE), 
	 CONSTRAINT "CAS_TOKEN_SESSION_PK" PRIMARY KEY ("UUID")
  USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1
  BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
  TABLESPACE "PORTALFUNDEWEB"  ENABLE
   ) SEGMENT CREATION IMMEDIATE 
  PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 
 NOCOMPRESS LOGGING
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1
  BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
  TABLESPACE "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.

En nuestro proyecto tendremos que incluir las entidades correspondientes e implementar UserDetails (en nuestro ejemplo son las clases de es.um.atica.proyecto.entities y la clase ServiceUserDetails de es.um.atica.logincas.model, las podemos descargar en el apartado Resto de clases). Además, si nuestro frontend necesita consultar datos de usuario, debemos implementar los métodos correspondientes en la clase UserEndpoints (en nuestro ejemplo sólo hay un /whoami básico).

Cosas a añadir en el proyecto

Dependencias en el pom.xml

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

Dependencias pom.xml
        <!-- 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:

application.properties
login.config.serviceurl=https://atica-67-165.atica.um.es:8080/api/entrada
login.config.sso.loginurl=https://sso.um.es/cas/login
login.config.sso.logouturl=https://sso.um.es/cas/logout
login.config.sso.serverurl=https://sso.um.es/cas/
es.um.jwt.secret=---
security.enable-csrf=false
es.um.token.session.expiration=1000
# Tiempo de vida del token (en minutos)
es.um.jwt.expires=15
server.servlet.session.persistent=false
# Entorno - Se comprobara si esta variable es "local" o "desarrollo"
es.um.p15s.environment=local

La variable login.config.serviceurl es a la que tendremos que acceder para redirigir al cas. Se utiliza un secret para codificar el token, en la variable es.um.jwt.secret. En cada entorno se sustituirá por el secret correspondiente. Si queremos probar el login con el portal, como será este el que nos devuelva el token cifrado, tendremos que poner el mismo secret que se utilice en el entorno correspondiente con el que hagamos las pruebas.

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:

Clase inicial - NombreProyectoApplication.java
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();
	}
}

Configuración de seguridad

Nuestra clase de configuración de seguridad (la que tenemos con la anotación @EnableWebSecurity, se llame ConfiguraciónSeguridad, SecurityConfig, o como queramos) quedará así (no hay que duplicar esta clase, debemos tener una en el proyecto, si tenemos otras cosas metidas en nuestro SecurityConfig, como ModelMapper por ejemplo, se debe añadir lo que contiene esta):

Configuración de seguridad - SecurityConfig.java
package es.um.atica.----.cas.config;
 
import static es.um.atica.----.Constants.SESSION_TOKEN_HEADER;
import static es.um.atica.----.Constants.LOCAL_ENV;
 
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 ConfiguracionSeguridad extends WebSecurityConfigurerAdapter {

	private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager
			.getLogger( ConfiguracionSeguridad.class );
 
    private final AuthenticationProvider authenticationProvider;
    private final LogoutFilter logoutFilter;
    private final SingleSignOutFilter singleSignOutFilter;
    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final ServiceProperties serviceProperties;
	@Value( "${es.um.p15s.environment}" )
	private String environment;
 
    @Autowired
    public ConfiguracionSeguridad ( 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 ) -> {
            String urlProtocol = "http:";
            if ( !LOCAL_ENV.equals( environment ) ) {
                urlProtocol = "https:";
            }
            final String url = context.getRequestURL().toString().replace( "http:", urlProtocol );
            log.error( "-- ORIGINAL Dynamic URL: " + context.getRequestURL().toString() + " ---" );
            return new ServiceAuthenticationDetails() {

                private static final long serialVersionUID = 1L;

                @Override
                public String getServiceUrl() {
                    log.error( "-- Dynamic URL: " + url + " ---" );
                    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;
    }
 
}

Descargar clases necesarias

Podemos descargar estas clases, y el resto de clases necesarias aquí: Clases cliente CAS.rar.

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í:

PaqueteClases
es.um.atica.proyectoConstants.java, NombreProyectoApplication.java, SecurityConfig.java
es.um.atica.proyecto.cas.entitiesObjetivos.java, Roles.java, RolesObjetivos, RolesUsuario.java, TokenSession.java, Usuarios.java
es.um.atica.proyecto.cas.exceptionsServicioException.java, ServicioNotFoundException.java, TokenExpiredException.java, UnauthorizedException.java, UsuarioNotFoundException.java
es.um.atica.proyecto.cas.handlers

CustomizedLogoutHandler.java, CustomizedUrlAuthenticationSuccessHandler.java

es.um.atica.proyecto.cas.modelServiceUserDetails.java
es.um.atica.proyecto.cas.repositoriesObjetivosRepository.java, RolesRepository.java, RolesObjetivosRepository, RolesUsuarioRepository.java, TokenSessionRepository.java, UsuariosRepository.java
es.um.atica.proyecto.cas.restSecuredEndpoint.java, SessionManagementendPoint.java, UserEndpoint.java
es.um.atica.proyecto.cas.servicesUsuarioService.java
es.um.atica.proyecto.cas.utilJwtTokenUtil.java, SessionTokenUtil.java, Util.java

Podemos descargarlas y meterlas en nuestro proyecto, cambiando los nombres de los paquetes para que coincidan con los de nuestro proyecto, así como los datos de las tablas de base de datos en las entidades.

Cómo comprobar el token

Recibiremos el token por parte del front-end como un String, en el header Authorization, que tendrá la siguiente forma:

# Bearer token
"Bearer tokenCifrado"

Para realizar la comprobaciones en cada petición que llegue, podemos utilizar filtros que añadiremos a la configuración de seguridad de nuestra aplicación.

Filtros

Para comprobar el token en cada petición que se haga, tendremos un filtro de autorización. Un ejemplo podría ser este:

Filtro de autorización
public class JwtAuthorizationFilter extends OncePerRequestFilter {

	private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager
			.getLogger( JwtAuthorizationFilter.class );

	@Autowired
	private ExpedientesService expedientesService;

	@Override
	protected boolean shouldNotFilter( HttpServletRequest request ) throws ServletException {
		final List<String> skipUrls = Arrays.asList( "/public/**" );
		final AntPathMatcher pathMatcher = new AntPathMatcher();
		return skipUrls.parallelStream().anyMatch( p -> pathMatcher.match( p, request.getServletPath() ) );
	}

	@Override
	protected void doFilterInternal( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
			FilterChain filterChain ) throws ServletException, IOException {

		final String token = httpServletRequest.getHeader( HEADER_AUTHORIZATION_KEY );

		// En getSivaIdFromToken se obtienen los datos del token, lanzando las excepciones correspondientes si éste es
		// incorrecto o ha expirado
		final String sivaId = expedientesService.getSivaIdFromToken( token );
		// Si no hay sivaId o es -1, se lanza una excepción
		if ( ( sivaId == null ) || sivaId.contentEquals( "-1" ) ) {
			throw new UnauthorizedException();
		}

		filterChain.doFilter( httpServletRequest, httpServletResponse );
	}
}

Como vemos, definimos dos métodos:

  • shouldNotFilter sirve para especificar las rutas en las que no queremos que se ejecute el filtro. En este caso, solo se especifica que todas las que estén bajo la ruta /public.
  • doFilterInternal es el filtro en sí, en él tendremos que incluir el código del filtro correspondiente a nuestra aplicación. Aquí se comprueba que el token es correcto y no está expirado, y que el usuario logeado tiene un sivaId, lanzando una excepción en caso de error. Si todo es correcto, llamamos a filterChain.doFilter(...), que sirve para continuar al siguiente filtro.

En el caso de que utilicemos login con cliente CAS, también incluiremos un filtro de autenticación, que verificará la identidad del usuario que realiza las peticiones. Tenemos el siguiente ejemplo:

Filtro de autenticación
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager
			.getLogger( JwtAuthenticationFilter.class );

	private final AuthenticationManager authenticationManager;

	public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
		this.authenticationManager = authenticationManager;
		super.setAuthenticationFailureHandler(new JwtAuthenticationFailureHandler());
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		log.info( "############ AUTHENTICATION: ATTEMTP" );
		try {
			final LoginUsuario userCredentials = new ObjectMapper().readValue(request.getInputStream(), LoginUsuario.class);

			return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
					userCredentials.getEmail(), userCredentials.getPassword()));
		} catch (final IOException e) {
			return null;
		}
	}

	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
		log.info( "############ AUTHENTICATION: SUCCESS" );

		final String token = TokenUtil.generaToken(authResult);
		response.addHeader(HEADER_AUTHORIZATION_KEY, TOKEN_BEARER_PREFIX + " " + token);
	}

}

Podemos ver que además del constructor, implementamos los métodos attempAuthentication successfulAuthentication en los que intentamos autenticar al usuario con sus credenciales y especificamos el comportamiento una vez se ha hecho la autenticación, respectivamente.

Atención: en las aplicaciones con login en el Portal de Servicios, falta por implementar la verificación del usuario que realiza las peticiones, en cuanto esté implementado lo añadiremos a esta guía.

Estos filtros tendremos que añadirlos en nuestra configuración de seguridad:

Filtros en configuración de seguridad
	/**
	 * Defino como Bean el authentication manager bean para que sea accedido desde los filtros ya que no estan en el mismo contexto
	 */
	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

	@Bean
	public JwtAuthorizationFilter jwtAuthorizationFilterBean() {
		return new JwtAuthorizationFilter();
	}

	/**
	 * Proveedor de gestion de acceoss
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // configuro politica de sesion sin estado
		.and().cors() // Aniado configuracion CORS por defecto
		.and().csrf().disable() // Deshabilito csrf ya que exponemos el api a cualquier dominio
		.authorizeRequests().antMatchers( "/public/**" ).permitAll() // Permito pasar sin autenticar todas las
		// peticiones que vayan a "/public"
		.anyRequest().authenticated() // El resto de peticiones deben ser autenticadas;
		//Filtro de atenticacion
		//Primer filtro intercepta peticiones de login
		.and().addFilterBefore(new JwtAuthenticationFilter(authenticationManagerBean()),UsernamePasswordAuthenticationFilter.class)
		//Segundo filtro valida el token
		.addFilterBefore( jwtAuthorizationFilterBean(), UsernamePasswordAuthenticationFilter.class );
	}

Como vemos, en el configure incluimos que a las rutas /public se permitan todas, lo que permite hacer llamadas a esos métodos sin necesidad de autenticar, es decir, en esas llamadas no se aplicará el filtro de autenticación. Para hacer lo mismo en el filtro de autorización, ya hemos visto que disponemos de un método en el propio filtro.

Obtener datos del token

Para obtener los datos del token, podemos obtener sus Claims, que contienen la información que se pasa en el token. Se utiliza el secret para descifrarlo. Para esto tenemos una clase JwtTokenUtil que contiene el método getClaim, que devuelve un objeto Claims o lanza una excepción si el token esta vacío, es incorrecto o ha expirado:

JwtTokenUtil
@Component
public class JwtTokenUtil {

	private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager
			.getLogger( JwtTokenUtil.class );

	@Value( "${es.um.jwt.secret}" )
	private String secret;

	/**
	 * Obtiene los claims del token. Si el token ha expirado, lanza una TokenExpiredException. Si el token no es válido,
	 * lanza una UnauthorizedException.
	 *
	 * @param token
	 * @return claims
	 */
	public Claims getClaim( String token ) {
		// Comprobamos que el token tiene contenido y empieza por "Bearer "
		if ( ( token == null ) || token.isEmpty() || !token.startsWith( TOKEN_BEARER_PREFIX ) ) {
			throw new UnauthorizedException();
		}
		token = token.replace( TOKEN_BEARER_PREFIX + " ", "" ); // Quitamos "Bearer" y el espacio en blanco

		try {
			// Obtenemos claims
			final Claims claims = Jwts.parser().setSigningKey( TextCodec.BASE64.encode( secret ) )
					.parseClaimsJws( token ).getBody();
			log.debug( "El claims es: " + claims.toString() );
			log.debug( "El subject es: " + claims.getSubject() );

			// Comprobamos si el token está expirado
			final Date expiration = claims.getExpiration();
			if ( Instant.now().isAfter( expiration.toInstant() ) ) {
				log.error( "Expiration Date: " + expiration.toString() );
				log.error( "Current Instant: " + Instant.now().toString() );
				log.error( "Error el token " + token + " está caducado" );
				throw new TokenExpiredException();
			}

			return claims;

		} catch ( final ExpiredJwtException e ) {
			log.error( "Error el token está caducado" );
			throw new TokenExpiredException();
		} catch ( final JwtException e ) {
			log.error( "Error filtering:" + e );
			throw new UnauthorizedException();
		}

	}
}

Como vemos, tiene que ir anotada con @Component, y después la instanciaremos en nuestros servicios o filtros con @Autowired. También vemos que cogemos el valor del secret y de cuándo expira de nuestro application.properties.

El secret lo utilizamos para decodificar el token, y obtenemos un objeto de tipo Claims. Podemos ver la información que contiene con toString(), pero lo más importante es obtener el email del usuario logeado, que lo haremos con .getSubject().

Si el token no es válido, o si ha expirado, se lanza una excepción. Para ello nos hemos definido excepciones propias, en un paquete exceptions.

Token en métodos REST

Con lo visto anteriormente, en las llamadas a nuestros métodos REST privados se comprobará automáticamente el token. Si queremos utilizarlo para la lógica que se haga en dicho método, debemos incluir como parámetro de la función el header Authorization (constante AUTHORIZATION_HEADER) como requerido, con @RequestHeader:

Cómo comprobar el token
	@GetMapping( PRIVATE_PREFIX + "/titulaciones" )
	public ResponseEntity getTitulaciones(
			@RequestHeader( value = AUTHORIZATION_HEADER, required = true ) String token ) {

		RestExpedientes.log.info( " ############################################### titulaciones: " );

		return new ResponseEntity<>( expedientesService.getTitulaciones( token ), HttpStatus.OK );
	}

Excepciones

Utilizamos las siguientes excepciones, que también se incluyen en el cliente CAS:

ServicioException.java

ServicioException
/**
 * ServicioNotFoundException
 */
public class ServicioException extends RuntimeException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.BAD_REQUEST;
 
	protected ServicioException( String msg ) {
		super( msg );
	}
 
	public HttpStatus getStatus() {
		return ServicioException.status;
	}
 
}

TokenExpiredException.java

TokenExpiredException
/**
 * ServicioNotFoundException
 */
public class TokenExpiredException extends ServicioException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.UNAUTHORIZED;
 
	public TokenExpiredException() {
		super( "Token_expired" );
	}
 
	@Override
	public HttpStatus getStatus() {
		return TokenExpiredException.status;
	}
 
}

UnauthorizedException.java

UnauthorizedException
/**
 * ServicioNotFoundException
 */
public class UnauthorizedException extends ServicioException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.UNAUTHORIZED;
 
	public UnauthorizedException() {
		super( "No tiene acceso para solicitar este servicio " );
	}
 
	@Override
	public HttpStatus getStatus() {
		return UnauthorizedException.status;
	}
 
}


  • Sin etiquetas