| Tabla de contenidos |
|---|
...
Actualmente, en el Portal de Servicios se utiliza el token Anteriormente utilizábamos un token generado por POSE, pero se está migrando para utilizar token OAuth. Por lo tanto, de momento tenemos que incluir ambos; nuestra aplicación tendrá login dual, aceptará tanto token POSE como token ha cambiado a OAuth, a partir del 21/03/2022 todo va con OAuth.
Login sólo con
...
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 nuestro proyecto. En local, debemos incluir el secret como una variable de entorno en Eclipse. Esto lo haremos yendo a Run As → Run Configurations..., tendremos que seleccionar el proyecto correspondiente en la barra de la izquierda, e irnos a la pestaña Environment. Ahí le damos a Add.., e introducimos la variable ES_UM_JWT_SECRET, y el correspondiente valor del secret:
Pulsamos en Apply, y podemos cerrar la ventana.
Después, incluiremos la variable es.um.jwt.secret en el archivo application.properties de nuestro proyecto, así:
| Bloque de código | ||
|---|---|---|
| ||
es.um.jwt.secret=${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. Al hacerlo de esta manera, evitamos que el secret quede expuesto en GitLab.
Para desarrollar en local, utilizaremos el token de micampusdesa, así que hará falta incluir el secret de desarrollo. Para obtener este secret, preguntad a MNCS, pues por seguridad no lo compartimos aquí.
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.
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:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
# 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.
Filtro de autorización
Para comprobar el token en cada petición que se haga, tendremos un filtro de autorización. Un ejemplo, podría ser este:
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
// RUTAS A NO FILTRAR: INCLUIR RUTAS PÚBLICAS Y RUTAS DE OAUTH, PUES SE FILTRAN DIRECTAMENTE
private List<String> skipUrls = Arrays.asList( "/public/**", "/mncs/consulta-expedientes-api/**" );
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
protected boolean shouldNotFilter( HttpServletRequest request ) throws ServletException {
return skipUrls.stream().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 );
// INCLUIR AQUÍ LAS COMPROBACIONES NECESARIAS DE LA APLICACIÓN
// Con getClaim sólo se comprueba si el token es correcto
try {
jwtTokenUtil.getClaim( token );
} catch ( ServicioException e ) {
httpServletResponse.setStatus( e.getStatus().value() );
return;
}
// OBLIGATORIO
filterChain.doFilter( httpServletRequest, httpServletResponse );
}
}
|
Este filtro lo utilizaremos como mínimo para comprobar el token POSE. Marcando la clase con @Component y extendiendo a OncePerRequestFilter, directamente se ejecutará en cada petición que se reciba.
Se deben ignorar rutas públicas y rutas con token OAuth, pues se filtran directamente, y si intentamos abrir un token OAuth con los métodos de token POSE saltará un error. Para saltarse estas rutas, tenemos en el código el array skipUrls. Aquí, tenemos que añadir la ruta base OAuth a mano, que en el ejemplo es /mncs/consulta-expedientes-api/**. Después, teniendo eso, el método shouldNotFilter se encarga de hacer match de la ruta a la que se ha llamado.
Después, si se ejecuta el filtro, se ejecuta el código que indiquemos en doFilterInternal. Aquí, como comentamos, por lo menos comprobaremos el token POSE llamando a jwtTokenUtil.getClaim, pero si por la lógica de la aplicación se necesita filtrar algo más, también se añadirá aquí.
Constantes
En las siguientes explicaciones se utilizan algunas constantes. Para definirlas, podemos tener una clases Constants.java, en el paquete base del proyecto, por ejemplo. Esta clase, únicamente con las constantes que se utilizan en esta explicación y las cadenas para los métodos REST públicos y privados, sería la siguiente:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
package es.um.atica.-----;
public final class Constants {
private Constants() {}
public static final String PUBLIC_PREFIX = "/public";
public static final String PRIVATE_PREFIX = "/private";
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String TOKEN_BEARER_PREFIX = "Bearer";
} |
Obtener datos del token
OAuth
Librería de cliente RRHH para obtener datos de usuario
En la mayoría de aplicaciones querremos obtener el identificador del usuario logeado para pedir datos a base de datos. Para obtener este identificador, rrhh dispone de un servicio, y tenemos una librería que hace de cliente de éste, facilitando la llamada sin tener que diferenciar entre el tipo de token que tenemos y permitiendo cachear las respuestas.
Para incluir la librería, en el pom.xml, en el bloque de dependencies, añadiremos esta:
| Bloque de código | ||
|---|---|---|
| ||
<!-- Servicios RRHH Client -->
<dependency>
<groupId>es.um.atica.fundewebjs.fundewebjs-api</groupId>
<artifactId>fundewebjs-serviciosrrhh-client</artifactId>
<version>1.0.2-SNAPSHOT</version>
</dependency> |
Y en el bloque de repositories, si no lo tenemos, este:
| Bloque de código | ||
|---|---|---|
| ||
<repository>
<id>fundewebjs.archiva.atica.umu.es</id>
<name>ATICA - UMU Repository - FundeWebJS</name>
<url>https://archiva.um.es/archiva/repository/FundeWebJS/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository> |
Debemos tener la clase inicial de la aplicación (NombreServicioApplication) en la notación @SpringBootApplication, el atributo scanBasePackages de la siguiente forma:
| Bloque de código |
|---|
@SpringBootApplication( scanBasePackages = {
"es.um.atica.[PAQUETE_APPLICATION]", "es.um.atica.fundewebjs"
} ) |
Y también tendremos que tener en el application.properties definida la variable app.env, que acepta valores desa, test o prod, según el entorno:
| Bloque de código |
|---|
app.env = desa |
Después, para utilizarla, simplemente la incluimos con @Autowired:
| Bloque de código |
|---|
@Autowired
private ServiciosRrhhClientService serviciosRrhhClient; |
Y llamamos al método getAfiliacion pasándole el subject del token, el token como String (cogido con @RequestHeader en nuestro método REST) y el Jwt (cogido con @AuthenticationPrincipal):
| Bloque de código | ||
|---|---|---|
| ||
// Si la aplicación tiene login dual
serviciosRrhhClient.getAfiliacion(subject, token, jwt);
// Versión sólo OAuth
serviciosRrhhClient.getAfiliacion(subject, jwt); |
Esto nos devuelve un objeto de tipo AfiliacionDTO, que contiene información del usuario. En concreto, se compone de estos campos:
| Bloque de código | ||
|---|---|---|
| ||
private String identificador;
private String letra;
private String tipoIdentificador;
private String nombre;
private String apellido1;
private String apellido2;
private Date fechaNacimiento;
private String sexo;
private String nacionalidad;
private DireccionDTO direccion;
private List<String> telefonos;
private List<String> emails;
private List<CentroAlumnoDTO> centros; |
Todos tienen sus correspondientes getters. Lo más normal es que utilicemos el identificador, pero los demás también pueden ser útiles. En el caso de los emails, si un usuario tiene varios, el primero de la lista es el principal.
Forma antigua
Dejamos temporalmente la documentación antigua, con el token POSE, por si hace falta para rescatar algún proyecto antiguo:
Login con token POSE
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 nuestro proyecto. En local, debemos incluir el secret como una variable de entorno en Eclipse. Esto lo haremos yendo a Run As → Run Configurations..., tendremos que seleccionar el proyecto correspondiente en la barra de la izquierda, e irnos a la pestaña Environment. Ahí le damos a Add.., e introducimos la variable ES_UM_JWT_SECRET, y el correspondiente valor del secret:
Pulsamos en Apply, y podemos cerrar la ventana.
Después, incluiremos la variable es.um.jwt.secret en el archivo application.properties de nuestro proyecto, así:
| Bloque de código | ||
|---|---|---|
| ||
es.um.jwt.secret=${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. Al hacerlo de esta manera, evitamos que el secret quede expuesto en GitLab.
Para desarrollar en local, utilizaremos el token de micampusdesa, así que hará falta incluir el secret de desarrollo. Para obtener este secret, preguntad a MNCS, pues por seguridad no lo compartimos aquí.
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.
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:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
# 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.
Filtro de autorización
Para comprobar el token en cada petición que se haga, tendremos un filtro de autorización. Un ejemplo, podría ser estePara 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 | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
@Component public class JwtAuthorizationFilter JwtTokenUtilextends OncePerRequestFilter { @Autowired 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 JwtTokenUtil jwtTokenUtil; // RUTAS A NO FILTRAR: INCLUIR RUTAS PÚBLICAS Y RUTAS DE OAUTH, PUES SE FILTRAN DIRECTAMENTE private List<String> skipUrls = Arrays.asList( "/public/**", "/mncs/consulta-expedientes-api/**" ); private AntPathMatcher pathMatcher = new AntPathMatcher(); @Override protected boolean shouldNotFilter( HttpServletRequest request ) throws ServletException { return skipUrls.stream().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 ); // INCLUIR AQUÍ LAS COMPROBACIONES NECESARIAS DE LA APLICACIÓN // Con getClaim sólo se comprueba si el token es correcto try { // Obtenemos claimsjwtTokenUtil.getClaim( token ); final Claims claims = Jwts.parser().setSigningKey( TextCodec.BASE64.encode( secret} catch ( ServicioException e ) ){ httpServletResponse.parseClaimsJwssetStatus( token e.getStatus().getBodyvalue() ); 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.
Con esto, un ejemplo de cómo obtener el email del usuario teniendo el token, sería así:
| Bloque de código |
|---|
@Autowired
private JwtTokenUtil jwtTokenUtil;
...
Claims claims = jwtTokenUtil.getClaim(token);
String email = claims.getSubject(); |
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 | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
@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
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
/**
* 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
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
/**
* 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
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
/**
* 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;
}
} |
Login con token OAuth
En primer lugar, debemos tener configurada nuestra aplicación para que soporte OAuth, lo haremos siguiendo esta guía: Migración del backend a soporte oAuth.
Una vez hecho esto, para obtener el token en nuestros servicios REST, utilizamos @AuthenticationPrincipal:
| Bloque de código | ||
|---|---|---|
| ||
@GetMapping( PRIVATE_PREFIX + API_VERSION + "/resguardos" )
public ResponseEntity getResguardos( @AuthenticationPrincipal Jwt jwt,
@RequestHeader( value = "Authorization", required = true ) String token ) { ... } |
Como vemos, tendremos tanto esto como el header. Cuando entremos con token POSE, jwt será null. Podemos obtener el identificador del token simplemente con:
| Bloque de código | ||
|---|---|---|
| ||
jwt.getSubject() |
Con la configuración que habremos añadido, se tienen dos endpoints:
| Bloque de código | ||
|---|---|---|
| ||
@RequestMapping( "/", "${app.server.path}" ) |
Esto es, la ruta base del proyecto para token POSE, y la ruta base + "/grupo/proyecto" para OAuth.
Aunque tengamos estos endpoints diferentes, los métodos no están duplicados, pero no será necesario distinguir qué tipo de token recibimos. Cada tipo de token puede llevar un identificador diferente, pero hemos hecho una librería que hace de cliente del servicio de recursos humanos para obtener el identificador de cada usuario, lo vemos en el siguiente apartado.
Librería de cliente RRHH para obtener datos de usuario
En la mayoría de aplicaciones querremos obtener el identificador del usuario logeado para pedir datos a base de datos. Para obtener este identificador, rrhh dispone de un servicio, y tenemos una librería que hace de cliente de éste, facilitando la llamada sin tener que diferenciar entre el tipo de token que tenemos y permitiendo cachear las respuestas.
Para incluir la librería, en el pom.xml, en el bloque de dependencies, añadiremos esta:
| Bloque de código | ||
|---|---|---|
| ||
<!-- Servicios RRHH Client -->
<dependency>
<groupId>es.um.atica.fundewebjs.fundewebjs-api</groupId>
<artifactId>fundewebjs-serviciosrrhh-client</artifactId>
<version>1.0.2-SNAPSHOT</version>
</dependency> |
Y en el bloque de repositories, si no lo tenemos, este:
| Bloque de código | ||
|---|---|---|
| ||
<repository>
<id>fundewebjs.archiva.atica.umu.es</id>
<name>ATICA - UMU Repository - FundeWebJS</name>
<url>https://archiva.um.es/archiva/repository/FundeWebJS/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository> |
Debemos tener la clase inicial de la aplicación (NombreServicioApplication) etiquetada con esto, sustituyendo expedienteacademico por el nombre del paquete en la aplicación:
| Bloque de código |
|---|
@ComponentScan( basePackages = {
"es.um.atica.expedienteacademico", "es.um.atica.fundewebjs"
} ) |
Y también tendremos que tener en el application.properties definida la variable app.env, que acepta valores desa, test o prod, según el entorno:
| Bloque de código |
|---|
app.env = desa |
Después, para utilizarla, simplemente la incluimos con @Autowired:
| Bloque de código |
|---|
@Autowired
private ServiciosRrhhClientService serviciosRrhhClient; |
Y llamamos al método getAfiliacion pasándole el subject del token, el token como String (cogido con @RequestHeader en nuestro método REST) y el Jwt (cogido con @AuthenticationPrincipal):
| Bloque de código | ||
|---|---|---|
| ||
// Si la aplicación tiene login dual
serviciosRrhhClient.getAfiliacion(subject, token, jwt);
// Versión sólo OAuth
serviciosRrhhClient.getAfiliacion(subject, jwt); |
Esto nos devuelve un objeto de tipo AfiliacionDTO, que contiene información del usuario. En concreto, se compone de estos campos:
| Bloque de código | ||
|---|---|---|
| ||
private String identificador;
private String letra;
private String tipoIdentificador;
private String nombre;
private String apellido1;
private String apellido2;
private Date fechaNacimiento;
private String sexo;
private String nacionalidad;
private DireccionDTO direccion;
private List<String> telefonos;
private List<String> emails;
private List<CentroAlumnoDTO> centros; |
return;
}
// OBLIGATORIO
filterChain.doFilter( httpServletRequest, httpServletResponse );
}
}
|
Este filtro lo utilizaremos como mínimo para comprobar el token POSE. Marcando la clase con @Component y extendiendo a OncePerRequestFilter, directamente se ejecutará en cada petición que se reciba.
Se deben ignorar rutas públicas y rutas con token OAuth, pues se filtran directamente, y si intentamos abrir un token OAuth con los métodos de token POSE saltará un error. Para saltarse estas rutas, tenemos en el código el array skipUrls. Aquí, tenemos que añadir la ruta base OAuth a mano, que en el ejemplo es /mncs/consulta-expedientes-api/**. Después, teniendo eso, el método shouldNotFilter se encarga de hacer match de la ruta a la que se ha llamado.
Después, si se ejecuta el filtro, se ejecuta el código que indiquemos en doFilterInternal. Aquí, como comentamos, por lo menos comprobaremos el token POSE llamando a jwtTokenUtil.getClaim, pero si por la lógica de la aplicación se necesita filtrar algo más, también se añadirá aquí.
Constantes
En las siguientes explicaciones se utilizan algunas constantes. Para definirlas, podemos tener una clases Constants.java, en el paquete base del proyecto, por ejemplo. Esta clase, únicamente con las constantes que se utilizan en esta explicación y las cadenas para los métodos REST públicos y privados, sería la siguiente:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
package es.um.atica.-----;
public final class Constants {
private Constants() {}
public static final String PUBLIC_PREFIX = "/public";
public static final String PRIVATE_PREFIX = "/private";
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String TOKEN_BEARER_PREFIX = "Bearer";
} |
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 | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
@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.
Con esto, un ejemplo de cómo obtener el email del usuario teniendo el token, sería así:
| Bloque de código |
|---|
@Autowired
private JwtTokenUtil jwtTokenUtil;
...
Claims claims = jwtTokenUtil.getClaim(token);
String email = claims.getSubject(); |
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 | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
@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
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
/**
* 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
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
/**
* 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
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
/**
* 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;
}
} |
Login con token OAuth
En primer lugar, debemos tener configurada nuestra aplicación para que soporte OAuth, lo haremos siguiendo esta guía: Migración del backend a soporte oAuth.
Una vez hecho esto, para obtener el token en nuestros servicios REST, utilizamos @AuthenticationPrincipal:
| Bloque de código | ||
|---|---|---|
| ||
@GetMapping( PRIVATE_PREFIX + API_VERSION + "/resguardos" )
public ResponseEntity getResguardos( @AuthenticationPrincipal Jwt jwt,
@RequestHeader( value = "Authorization", required = true ) String token ) { ... } |
Como vemos, tendremos tanto esto como el header. Cuando entremos con token POSE, jwt será null. Podemos obtener el identificador del token simplemente con:
| Bloque de código | ||
|---|---|---|
| ||
jwt.getSubject() |
Con la configuración que habremos añadido, se tienen dos endpoints:
| Bloque de código | ||
|---|---|---|
| ||
@RequestMapping( "/", "${app.server.path}" ) |
Esto es, la ruta base del proyecto para token POSE, y la ruta base + "/grupo/proyecto" para OAuth.
Aunque tengamos estos endpoints diferentes, los métodos no están duplicados, pero no será necesario distinguir qué tipo de token recibimos. Cada tipo de token puede llevar un identificador diferente, pero hemos hecho una librería que hace de cliente del servicio de recursos humanos para obtener el identificador de cada usuario, lo vemos en el siguiente apartadoTodos tienen sus correspondientes getters. Lo más normal es que utilicemos el identificador, pero los demás también pueden ser útiles. En el caso de los emails, si un usuario tiene varios, el primero de la lista es el principal.
