Versiones comparadas

Clave

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

...

Bloque de código
languagejava
themeEclipse
# Bearer token
"Bearer asdffsdfasdfasdfasdfasdfasdfasdftokenCifrado"

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 esteEn nuestros métodos REST privados, por lo tanto, tendremos que incluir el header Authorization (constante AUTHORIZATION_HEADER) como requerido, con @RequestHeader, y podemos comprobarlo de la siguiente manera:

Bloque de código
languagejava
themeEclipse
titleCómo comprobar el tokenFiltro de autorización
linenumberstrue
collapsetrue
importpublic static es.um.atica.----.Constants.AUTHORIZATION_HEADER;

@GetMapping("/titulaciones")
public ResponseEntity titulaciones(@RequestHeader(value=AUTHORIZATION_HEADER, required = true) String tokenCodificado) {
	// Comprobamos el token
	try {
		jwtTokenUtil.getClaim( tokenCodificado );
	} catch( final ServicioException e ) {
		log.error( e.getLocalizedMessage() );
		return new ResponseEntity<>( e.getMessage(), e.getStatus(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() ) );
	}
}

En el método getClaim se comprueba si el token es correcto, y si lo es devuelve los datos que contiene. Este método se incluye en la clase JwtTokenUtil que viene en el cliente CAS, y si lo hacemos con el Portal de Servicios podemos incluirla así:

Bloque de código
languagejava
themeEclipse
titleJwtTokenUtil
linenumberstrue
collapsetrue
@Log4j2
@Component
public class JwtTokenUtil {

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

	// In minutes
	@Value( "${es.um.jwt.expires}" )
	private long expires;

	/**
	 * Get the claim from the token If token expirated throw TokenExpiredException If token is invalid throw
	 * UnauthorizedException
	 */
	public Claims getClaim( String token ) {
		final String[] tokenSplits = token.split( " " );
		if ( !tokenSplits[0].equals( "Bearer

	@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();
		} else {

			token = tokenSplits[1]filterChain.doFilter( httpServletRequest, httpServletResponse );
			try {
				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() );

				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 nuestro controlador REST con @Autowired. También vemos que cogemos el valor del secret y de cuándo expira de nuestro application.properties. Podemos ver cómo funciona la parametrización en esta página de la wiki.

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().

}
}

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:

Bloque de código
languagejava
themeEclipse
titleFiltro de autenticación
linenumberstrue
collapsetrue
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:

Bloque de código
languagejava
themeEclipse
titleFiltros en configuración de seguridad
linenumberstrue
collapsetrue
	/**
	 * 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:

Bloque de código
languagejava
themeEclipse
titleJwtTokenUtil
linenumberstrue
collapsetrue
@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:

Bloque de código
languagejava
themeEclipse
titleCómo comprobar el token
linenumberstrue
collapsetrue
	@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 CASSi 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. Estas también se incluyen en el cliente CAS, Las clases correspondientes son las siguientes:

ServicioException.java

Bloque de código
languagejava
themeEclipse
titleServicioException
linenumberstrue
collapsetrue
/**
 * 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;
	}
 
}

Token ExpiredExceptionTokenExpiredException.java

Bloque de código
languagejava
themeEclipse
titleTokenExpiredException
linenumberstrue
collapsetrue
/**
 * 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;
	}
 
}

...

Bloque de código
languagejava
themeEclipse
titleUnauthorizedException
linenumberstrue
collapsetrue
/**
 * 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;
	}
 
}

...