En este apartado añadiremos la configuración y código necesario para securizar con JWT una API desarrollada con Spring Boot. El punto de partida puede ser cualquier proyecto Spring Boot que funcione como una API REST.
Todo el apartado se basa en proyecto library-api-sec que hay disponible en el repositorio de spring-web
Securizando la API, el flujo al utilizarla se modifica un poco. Ahora nuestra intención es que las operaciones solamente puedan ser invocadas por usuarios identificados (exceptuando los endpoints para registrarse y obtener el token). Asi, nuestra API quedará protegida de forma que solamente los usuarios registrados podan hacer uso de ella (también podemos disponer de operaciones abiertas y otras securizadas, e incluso mediante el uso de roles). Para aquellas operaciones que se encuentren securizadas, será necesario añadir un Bearer token (JWT) como valor para la cabecera Authorization. Y para ello se deberá disponer de un usuario y contraseña.
En la API de ejemplo, añadiremos también un endpoint para el registro de usuario. Asi, podremos registrarnos, solicitar el token e invocar a cualquiera de las operaciones securizadas de esta API. Todo aquello siguiendo el flujo que se muestra a continuación:
. . . <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> . . .
En este paso crearemos los certificados que se utilizarán para firmar (privado) y para comprobar el token (público).
Más adelante, los añadiremos el fichero de propiedades application.properties para cargarlos cuando hagan falta en el fichero de configuración global de la API LibraryConfig.java.
# Crear un par de claves rsa (keypair.pem) openssl genrsa -out keypair.pem 2048 # Crear un certificado público (app.pub) openssl rsa -in keypair.pem -pubout -out app.pub # Crear un certificado privado (app.key) openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out app.key
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity( // securedEnabled = true, // jsr250Enabled = true, prePostEnabled = true) public class LibraryConfig extends WebSecurityConfigurerAdapter { @Value("${key.public}") RSAPublicKey key; @Value("${key.private}") RSAPrivateKey priv; @Autowired private LibraryUserDetailsService libraryUserDetailsService; @Autowired private AuthEntryPointJwt unauthorizedHandler; @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(libraryUserDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests().antMatchers("/register", "/token", "/h2-console/**").permitAll() .anyRequest().authenticated(); http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withPublicKey(this.key).build(); } @Bean JwtEncoder jwtEncoder() { JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build(); JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk)); return new NimbusJwtEncoder(jwks); } }
Contiene los métodos de utilidad generateJwtToken para generar un token (para los casos en los que se solicite) y validateJwtToken para verificar uno existente (para comprobar el que se indique en cualquier operación).
Se apoya en los Beans JwtEncoder y JwtDecoder que hemos definido en LibraryConfig.java
@Component @Configuration public class JwtUtils { private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); @Autowired private JwtEncoder jwtEncoder; @Autowired private JwtDecoder jwtDecoder; public String generateJwtToken(Authentication authentication) { Instant now = Instant.now(); String scope = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(" ")); JwtClaimsSet claims = JwtClaimsSet.builder() .issuer("self") .issuedAt(now) .expiresAt(now.plus(1, ChronoUnit.HOURS)) .subject(authentication.getName()) .claim("scope", scope) .build(); return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); } public String getUserNameFromJwtToken(String token) { return jwtDecoder.decode(token).getSubject(); } public boolean validateJwtToken(String authToken) { try { Jwt jwt = jwtDecoder.decode(authToken); return true; } catch (SignatureException e) { logger.error("Invalid JWT signature: {}", e.getMessage()); } catch (MalformedJwtException e) { logger.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { logger.error("JWT token is expired: {}", e.getMessage()); } catch (UnsupportedJwtException e) { logger.error("JWT token is unsupported: {}", e.getMessage()); } catch (IllegalArgumentException e) { logger.error("JWT claims string is empty: {}", e.getMessage()); } return false; } }
Aqui definimos el formato que queremos para la respuesta a la petición de token. En este caso se devuelve el propio token (JWT), el nombre de usuario de quien lo solicita y los roles que tiene asignados (por ahora ninguno)
@Data @AllArgsConstructor @NoArgsConstructor public class JwtResponse { private String token; private String username; private List<String> roles; }
En esta clase, en el método commence se define cómo se quieren tratar los errores de autenticación. En este caso se registra una traza en en log y se responde con una respuesta vacía y el código de estado 401 Unauthorized.
@Component public class AuthEntryPointJwt implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { logger.error("Unauthorized error: {}", authException.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); } }
public class AuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils; @Autowired private LibraryUserDetailsService userDetailsService; private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = parseJwt(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { String username = jwtUtils.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception exception) { logger.error("Cannot set user authentication: {}", exception); } filterChain.doFilter(request, response); } private String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7); } return null; } }
@Service public class LibraryUserDetailsService implements UserDetailsService { @Autowired private UserService userService; @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userService.findByUsername(username); if (user == null) throw new UsernameNotFoundException("Invalid username/password"); List<GrantedAuthority> authorities = getUserAuthority(user.getRoles()); return buildUserForAuthentication(user, authorities); } private List<GrantedAuthority> getUserAuthority(Set<Role> userRoles) { Set<GrantedAuthority> roles = new HashSet<>(); userRoles.forEach(role -> roles.add(new SimpleGrantedAuthority(role.getName()))); return new ArrayList<>(roles); } private UserDetails buildUserForAuthentication(User user, List<GrantedAuthority> authorities) { return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.isActive(), true, true, true, authorities); } }
spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.globally_quoted_identifiers=true server.port=8080 spring.datasource.url=jdbc:h2:file:/Users/santi/library_sec.db spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true key.private=classpath:app.key key.public=classpath:app.pub
@RequestMapping(value = "/register", method = RequestMethod.POST) public ResponseEntity<?> saveUser(@RequestBody UserDTO user) throws Exception { return ResponseEntity.ok(userService.addUser(user)); }
@PostMapping("/token") public ResponseEntity<?> authenticateUser(@Valid @RequestBody UserDTO userDTO) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(userDTO.getUsername(), userDTO.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtUtils.generateJwtToken(authentication); User userDetails = (User) authentication.getPrincipal(); List<String> roles = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getUsername(), roles)); }
Existe un ejemplo de API securizada con JWT tal y como se explica en este apartado. Está en el repositorio spring-web bajo el nombre library-api-sec
© 2022-2025 Santiago Faci