Introducción
En el mundo de las aplicaciones web, la seguridad es un aspecto fundamental que no se puede pasar por alto. La autenticación y la autorización son pilares esenciales para garantizar que solo los usuarios autorizados tengan acceso a los recursos críticos de nuestras aplicaciones. Una de las tecnologías más utilizadas y efectivas para manejar la autenticación es JSON Web Token (JWT). En este post, exploraremos cómo implementar JWT en una aplicación Java utilizando Spring Boot y Maven.
A través de este tutorial, aprenderás a integrar JWT en tu proyecto de manera sencilla y eficiente, reforzando la seguridad de tu aplicación. Utilizaremos Maven para gestionar las dependencias y construir nuestro proyecto, lo que facilitará su mantenimiento y escalabilidad. Cabe mencionar que esta es solo una de las muchas formas en las que se puede implementar la autenticación con JWT, pero es una de las más populares debido a su robustez y flexibilidad.
Base de datos
Para poder identificar a los usuarios de nuestra aplicación tendremos que tener tablas donde buscar dichos usuarios. Además, en este caso, estos usuarios también podran cumplir una serie de roles por lo que tendremos que tener una tabla propia en la que almacenar dichos roles.
Tabla de usuarios:
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(45) COLLATE utf8mb3_spanish2_ci NOT NULL,
`password` varchar(45) COLLATE utf8mb3_spanish2_ci NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username_UNIQUE` (`username`)
)
Tabla de roles:
CREATE TABLE `roles` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(15) COLLATE utf8mb3_spanish2_ci NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `nombre_UNIQUE` (`name`)
)
Tabla de relación:
Esta tabla se crea a consecuencia de la relación muchos a muchos entre usuarios y roles.
/*Esta tabla representa la relación muchos a muchos entre usuarios y roles
(puede haber un usuario con muchos roles y un rol con muchos usuarios)*/
CREATE TABLE `user_roles` (
`user_id` int NOT NULL,
`role_id` int NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `FK_roleID_idx` (`role_id`),
CONSTRAINT `FK_roleID` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT `FK_userID` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
)
Dependencias
Para implementar JWT en un proyecto Java con Spring Boot necesitaremos algunas dependencias mínimas en nuestro archivo pom.xml.
- Spring Web
- Spring Security
- Spring Data JPA
- MySQL Driver
- Lombok
- Json Web Token
Configuración del proyecto
Lo primero que tendremos que hacer una vez que tengamos todas las dependencias correctamente instaladas en nuestro pom.xml será añadir la configuración de la base de datos al archivo application.properties para que JPA pueda conectarse
Configuración de application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/jwtDB
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Estructura de la aplicación
En este caso para implementar el sistema de autenticación con Json Web Token vamos a crear una carpeta llamada Security donde tendremos todos los archivos de configuración de seguridad de la aplicación y un sistema básico de capas estructurado en carpetas donde almacenaremos nuestros modelos, repositorios, servicios y controladores.
📁com.example.jwtService
📁security
├── SecurityConfig.java
├── JWTGenerator.java
├── JWTAuthenticationFilter.java
📁models
├── UserModel.java
├── RoleModel.java
├── RegisterRequestModel.java
├── LoginRequestModel.java
├── AuthResponseModel.java
📁repository
├── UserRepository.java
├── RoleRepository.java
📁services
├── AuthService.java
├── UserService.java
📁controllers
├── AuthController.java
├── UserController.java
Código de la aplicación
-
Modelos
El modelo es una clase que representa entidades del dominio de la aplicación. Estas clases contienen atributos que corresponden a las propiedades de las entidades y pueden incluir lógica de negocio básica como validaciones.
UserModel.java
@Data
@Entity
@Table(name="users")
public class UserModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name="user_roles", joinColumns = @JoinColumn(name="user_id", referencedColumnName = "id" ),
inverseJoinColumns = @JoinColumn(name="role_id", referencedColumnName = "id"))
private List<RoleModel> roles = new ArrayList<>();
}
RoleModel.java
@Data
@Entity
@Table(name="roles")
public class RoleModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
String name;
}
RegisterRequestModel.java
@Data
public class RegisterRequestModel {
String username;
String password;
}
LoginRequestModel.java
public class LoginRequestModel extends RegisterRequestModel {}
AuthResponseModel.java
@Data
public class AuthResponseModel {
private String accesToken;
private String tokenType = "Bearer ";
private String username;
public AuthResponse(String token){
accesToken = token;
}
}
-
Repositorios
El repositorio se encarga de la persistencia de los datos. Es la capa que maneja las operaciones de acceso a la base de datos. En nuestro caso, utilizaremos Spring Data JPA, que proporciona interfaces para interactuar con la base de datos.
UserRepository.java
public interface UserRepository extends JpaRepository<UserModel, Integer> {
Optional<UserModel> findByUsername(String username);
Boolean existsByUsername(String username);
}
RoleRepository.java
public interface RoleRepository extends JpaRepository<RoleModel, Integer>{
Optional<RoleModel> findByName(String name);
}
-
Servicios
Un servicio contiene la lógica de negocio de la aplicación. Es una capa intermedia entre el controlador y el repositorio que realiza operaciones más complejas utilizando los métodos del repositorio. Los servicios permiten encapsular la lógica de negocio y reutilizarla en diferentes partes de la aplicación.
AuthService.java
@Service
public class AuthService implements UserDetailsService{
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserModel user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("No se ha encontrado al usuario"));
return new User(user.getUsername(), user.getPassword(), mapRoles(user.getRoles()));
}
private Collection<GrantedAuthority> mapRoles(List<RoleModel> roles){
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
}
}
UserService.java
@Service
public class UserService{
@Autowired
private UserRepository userRepository;
public List<UserModel>findAll(){
return userRepository.findAll();
}
}
-
Controladores
El controlador maneja las solicitudes HTTP, procesa la entrada del usuario, y devuelve respuestas HTTP. Es la capa que interactúa con el cliente (por ejemplo, un navegador web) y utiliza los servicios para realizar operaciones y obtener datos.
AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JWTGenerator jwtGenerator;
@PostMapping("/login")
public ResponseEntity<AuthResponseModel> login(@RequestBody LoginRequestModel loginRequest){
//Buscamos si el usuario existe y si la contraseña es correcta
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
//Generamos el JWT
String token = jwtGenerator.generateToken(authentication);
//Creamos la respuesta y la enviamos
AuthResponseModel authResponse = new AuthResponseModel(token);
authResponse.setUsername(loginRequest.getUsername());
return new ResponseEntity<>(authResponse, HttpStatus.OK);
}
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody RegisterRequestModel registerRequest){
//Comprobamos que el nombre de usuario no exista
if(userRepository.existsByUsername(registerRequest.getUsername())){
return new ResponseEntity<>("El usuario ya existe", HttpStatus.BAD_REQUEST);
}
//En caso de que no exista creamos un nuevo objeto de tipo usuario y lo almacenamos en la base de datos
UserModel user = new UserModel();
user.setUsername(registerRequest.getUsername());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
RoleModel roles = roleRepository.findByName("User").get();
user.setRoles(Collections.singletonList(roles));
userRepository.save(user);
return new ResponseEntity<>("El usuario se ha creado con éxito", HttpStatus.CREATED);
}
}
UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
UserService userService;
@GetMapping
public List<UserModel> GetAllUsers(){
return userService.findAll();
}
}
-
Configuración de seguridad
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private AuthService authService;
//Creamos el filtro de seguridad y lo configuramos de manera que todas las rutas requieran de autenticación a excepción de las que estén bajo /api/auth/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf((csrf) -> csrf.disable())
.authorizeHttpRequests((auth)->auth.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.sessionManagement((session)->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception{
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public JWTAuthenticationFilter jwtAuthenticationFilter(){
return new JWTAuthenticationFilter();
}
}
JWTGenerator.java
@Component
public class JWTGenerator {
//Creamos algunos parámetros del token (estos podrían ir en un archivo a parte o en variables de entorno)
public final long JWT_EXPIRATION = 60000 * 60 * 10;
public String JWTSECRET = "tuclavesecreta";
public String generateToken(Authentication auth){
String username = auth.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + JWT_EXPIRATION);
String token = Jwts.builder()
.subject(username)
.issuedAt(currentDate)
.expiration(expireDate)
.signWith(getSigningKey())
.compact();
return token;
}
public String getSubjectFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token);
return true;
} catch (Exception e) {
throw new AuthenticationCredentialsNotFoundException("El token ha expirado");
}
}
public SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(JWTSECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
}
JWTAuthenticationFilter.java
public class JWTAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JWTGenerator jwtGenerator;
@Autowired
private AuthService authService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = getJWTFromRequest(request);
if(StringUtils.hasText(token) && jwtGenerator.validateToken(token)){
String username = jwtGenerator.getSubjectFromToken(token);
UserDetails userDetails = authService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails.getUsername(),
null,
userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
private String getJWTFromRequest(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}