...
| Bloque de código |
|---|
|
# 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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | Cómo comprobar el tokenFiltro de autorización |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | JwtTokenUtil |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
@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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | Filtro de autenticación |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
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 y 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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | Filtros en configuración de seguridad |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
/**
* 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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | JwtTokenUtil |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
@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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | Cómo comprobar el token |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
@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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | ServicioException |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
/**
* 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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | TokenExpiredException |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
/**
* 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 |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | UnauthorizedException |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
|
/**
* 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;
}
} |
...