Implementación de seguridad y roles con Spring Security, JWT y MySQL

Implementación de seguridad en el backend de una aplicación con Spring Security utilizando JWT (JSON Web Token) para autenticación y creación de roles para autorización.

Overview

Al final de este tutorial vamos a conocer como implementar Spring Security con JWT en cualquier proyecto que deseemos, con una arquitectura altamente escalable. Se crea un login con el cual podemos autenticarnos y obtener un token de autorización dependiendo del rol (Admin o User) . Recomiendo ver CRUD con Java Spring Boot y MySQL, ya que es el proyecto que vamos a usar como base para esta ocasión, además de explicar conceptos claves para este tutorial.

¿Qué es Spring Security?

Spring Security es un framework de apoyo de Spring que provee una serie de servicios de seguridad destinados principalmente, a comprobar la identidad del usuario mediante la autenticación y los permisos asociados al mismo mediante la autorización.

Autorización y Autenticación

No se deben confundir los dos términos, la autorización es dependiente de la autenticación ya que se produce posteriormente a su proceso. Con JWT (JSON Web Token) básicamente lo que obtenemos es esa autorización a realizar diferentes acciones en nuestro sistema.

JWT

JWT (JSON Web Token) es un estándar que define un mecanismo para poder propagar la identidad de un usuario entre dos partes, y de forma segura, además con una serie de claims o privilegios.

Estructura básica de un JWT

Un JSON Web Token está formado por:

  • Cabecera (Header): En la cabecera se especifica el algoritmo de encriptación utilizado y el tipo de token.
  • Datos (Payload): Datos del usuario y privilegios, así como el resto de información.
  • Firma (Signature): La firma es única y nos permite verificar que el remitente es quien dice ser, además de que la data no se ha modificado en el camino, se utiliza HMACSHA256 para realizar un Hash, incluyendo cifrado con el algoritmo  SHA de 256 bits

Requerimientos

  • Conocimientos en Java Intermedios.
  • Conocimientos en Spring Intermedios.
  • Proyecto Base: Que lo encuentras en este tutorial CRUD con Java Spring Boot y MySQL, o lo puedes clonar directamente del repositorio de GitHub aquí
  • MySQL
  • Postman

Creación de paquete security

Creamos un paquete security donde vamos a manejar todo el tema de la seguridad

Estructura del proyecto

Ubicados en el paquete security vamos a crear los siguientes paquetes:

  • controller: Paquete donde exponemos los endpoints para hacer el login y crear un nuevo usuario
  • dto: Objeto de transferencia de datos, explicado con mas detalle en el anterior tutorial.
  • entity: Paquete que contiene las clases que representan las tablas de la base de datos.
  • enums: Paquete que contiene la clase donde se definen los Roles
  • jwt: Paquete que se encarga de almacenar las clases que tienen relación con JWT
  • repository: Paquete que contiene las interfaces que extienden de JpaRepository
  • service; Implementación de las interfaces del repository.
  • util: Paquete de utilidad que nos va a servir para crear los roles sin necesidad de consulta.

Adicional creamos una clase en security -> new Java Class -> MainSecurity.java Esta clase se encarga de enlazar todo y configurar lo necesario.

Estructura dentro del paquete security

Creación de clases

estructura-clases-spring-security
Estructura de clases security
  • controller:
    • AuthController: Clase que contiente los endpoints para hacer login y crear un nuevo usuario.
  • dto:
    • JwtDto: Clase que hace de DTO para el token.
    • LoginUsuario: Clase que hace de DTO para el login de usuario.
    • NuevoUsuario: Clase que hace de DTO para el nuevo usuario.
  • entity:
    • Rol: Representa la tabla Rol
    • Usuario: Representa la tabla Usuario
    • UsuarioMain: Clase que implementa los privilegios de cada usuario, se crea para mantener el principio de única responsabilidad.
  • enums:
    • RolNombre: Clase que contiene dos Rol Enum
  • jwt:
    • JwtEntryPoint: Clase que comprueba si existe un token si no devuelve un 401 no autorizado
    • JwtProvider: Clase que genera el token y valida que este bien formado y no este expirado
    • JwtTokenFilter: Clase que utliza el JwtProvider para validar que el token sea valido, y permitir los accesos a los diferentes recursos.
  • repository: Interfaces
    • RolRepository: Interfaz que extiende de JpaRepository para ayudarnos con el tema de persistencia.
    • UsuarioRepository: Interfaz que extiende de JpaRepository para ayudarnos con el tema de persistencia.
  • service:
    • RolService: Implementación de métodos de RolRepository.
    • UserDetailsServiceImpl: Clase que convierte la clase usuario en un UsuarioMain. UserDetailsService es propia de Spring Security.
    • UsuarioService: Implementación de métodos de UsuarioService.
  • util:
    • CreateRoles.java: Clase que sirve para crear los roles sin necesidad de hacer una consulta sql.

Configuración build.gradle

Añadimos dos dependencias nuevas:

implementation 'org.springframework.boot:spring-boot-starter-security:2.3.1.RELEASE'
implementation 'io.jsonwebtoken:jjwt:0.9.1'

Al momento de aplicar los cambios de estas dependencias y queramos consumir un servicio automáticamente nos va a dar el siguiente error:

{
“timestamp”: “2020-07-24T01:37:15.672+00:00”,
“status”: 401,
“error”: “Unauthorized”,
“message”: “Unauthorized”,
“path”: “/torre/listaTorre”
}

Spring security automáticamente no me deja acceder a los recursos de mi @controller

Configuración application.properties

# Security
# Variable que se usa para la firma de seguridad
jwt.secret = secret

#tiempo de expiración serial (12 horas) tiempo en seg
jwt.expiration = 43200

Agregamos el secreto de JWT explicado anteriormente, y un tiempo de expiración en segundos. Por lo tanto el application.properties se vería de la siguiente manera:

spring.datasource.url= jdbc:mysql://localhost:3306/crud?useSLL=false&serverTimezone=UTC&useLegacyDateTimeCode=false

#username and password
spring.datasource.username = root
spring.datasource.password =

#Show sql queries
spring.jpa.show-sql = true 

#update database and create entities
spring.jpa.hibernate.ddl-auto = update

#generate optimization hibernate SQL
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

# Security
# Variable que se usa para la firma de seguridad
jwt.secret = secret

#tiempo de expiración serial (12 horas) tiempo en seg
jwt.expiration = 43200

Clase Enum de Roles

En la clase RolNombre del paquete enums agregamos los roles de la aplicación:

package com.amoelcodigo.crud.security.enums;

public enum RolNombre {
    //El aplicativo tiene dos roles:
    // admin(Tiene todos los permisos CRUD) y user(ve las torres)
    ROLE_ADMIN, ROLE_USER
}

Entidades

Recordemos que hibernate SQL al tenerlo en update crea las tablas a partir de las @Entity, en el paquete entity:

package com.amoelcodigo.crud.security.entity;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.HashSet;
import java.util.Set;

/**
 * Clase para la base de datos
 */
@Entity
public class Usuario {
    //Id de la tabla
    @Id
    //Id Auto Increment
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idUsuario;
    //Decorador para indicar que no puede ser null el campo
    @NotNull
    private String nombre;
    @NotNull
    @Column(unique = true)
    private String nombreUsuario;
    @NotNull
    @Column(unique = true)
    private String email;
    @NotNull
    private String password;

    @NotNull
    //Relación many to many
    //Un usuario puede tener MUCHOS roles y un rol puede PERTENECER a varios usuarios
    //Tabla intermedia que tiene dos campos que va a tener idUsuario y idRol
    @ManyToMany
    // join columns hace referencia a la columna que hace referencia hacia esta
    // Es decir la tabla usuario_rol va a tener un campo que se llama id_usuario
    // inverseJoinColumns = el inverso, hace referencia a rol
    @JoinTable(name = "usuario_rol", joinColumns = @JoinColumn(name = "id_usuario"),
    inverseJoinColumns = @JoinColumn(name = "rol_id"))
    private Set<Rol> roles = new HashSet<>();

    public Usuario() {
    }

    //Constuctor sin Id ni Roles
    public Usuario(@NotNull String nombre, 
                   @NotNull String nombreUsuario, 
                   @NotNull String email, 
                   @NotNull String password) {
        this.nombre = nombre;
        this.nombreUsuario = nombreUsuario;
        this.email = email;
        this.password = password;
    }

    public int getIdUsuario() {
        return idUsuario;
    }

    public void setIdUsuario(int idUsuario) {
        this.idUsuario = idUsuario;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public String getUsuario() {
        return nombreUsuario;
    }

    public void setUsuario(String usuario) {
        this.nombreUsuario = usuario;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Set<Rol> getRoles() {
        return roles;
    }

    public void setRoles(Set<Rol> roles) {
        this.roles = roles;
    }
}
package com.amoelcodigo.crud.security.entity;

import com.amoelcodigo.crud.security.enums.RolNombre;

import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
public class Rol {
    @Id
    @GeneratedValue(strategy =  GenerationType.IDENTITY)
    private int id;

    @NotNull
    //Se indica que va a ser un Enum de tipo String
    @Enumerated(EnumType.STRING)
    private RolNombre rolNombre;

    public Rol() {
    }

    public Rol(@NotNull RolNombre rolNombre) {
        this.rolNombre = rolNombre;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public RolNombre getRolNombre() {
        return rolNombre;
    }

    public void setRolNombre(RolNombre rolNombre) {
        this.rolNombre = rolNombre;
    }
}
package com.amoelcodigo.crud.security.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.Column;
import javax.validation.constraints.NotNull;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Clase Encargada de generar la seguridad
 * Clase que implementa los privilegios de cada usuario
 * UserDetails es una clase propia de Spring Security
 */
public class UsuarioMain implements UserDetails {

    private String nombre;
    private String usuario;
    private String email;
    private String password;
    // Variable que nos da la autorización (no confundir con autenticación)
    // Coleccion de tipo generico que extendiende
    // de GranthedAuthority de Spring security
    private Collection<? extends GrantedAuthority> authorities;

    //Constructor
    public UsuarioMain(String nombre, String usuario, String email, String password, Collection<? extends GrantedAuthority> authorities) {
        this.nombre = nombre;
        this.usuario = usuario;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
    }

    //Metodo que asigna los privilegios (autorización)
    public static UsuarioMain build(Usuario usuario){
        //Convertimos la clase Rol a la clase GrantedAuthority
        List<GrantedAuthority> authorities =
                usuario.getRoles()
                        .stream()
                        .map(rol -> new SimpleGrantedAuthority(rol.getRolNombre().name()))
                .collect(Collectors.toList());
        return new UsuarioMain(usuario.getNombre(), usuario.getUsuario(), usuario.getEmail(),
                usuario.getPassword(), authorities);
    }

    //@Override los que tengan esta anotación
      // significa que son metodos de UserDetails de SpringSecurity
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return usuario;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public String getNombre() {
        return nombre;
    }

    public String getEmail() {
        return email;
    }
}

Paquete Repository

En las interfaces de los repositorios hacemos lo siguiente

package com.amoelcodigo.crud.security.repository;

import com.amoelcodigo.crud.security.entity.Rol;
import com.amoelcodigo.crud.security.enums.RolNombre;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;
//Notación que indica que es un repositorio
@Repository
public interface RolRepository extends JpaRepository<Rol, Integer> {

    Optional<Rol> findByRolNombre(RolNombre rolNombre);
}
package com.amoelcodigo.crud.security.repository;

import com.amoelcodigo.crud.security.entity.Usuario;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UsuarioRepository extends JpaRepository<Usuario, Integer> {

    Optional<Usuario> findByNombreUsuario(String nombreUsuario);
    boolean existsByNombreUsuario (String nombreUsuario);
    boolean existsByEmail (String email);
}

Paquete Service

package com.amoelcodigo.crud.security.service;

import com.amoelcodigo.crud.security.entity.Rol;
import com.amoelcodigo.crud.security.enums.RolNombre;
import com.amoelcodigo.crud.security.repository.RolRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.Optional;

@Service
@Transactional
public class RolService {

    @Autowired
    RolRepository rolRepository;

    public Optional<Rol> getByRolNombre(RolNombre rolNombre){
        return  rolRepository.findByRolNombre(rolNombre);
    }

    public void save(Rol rol){
        rolRepository.save(rol);
    }
}
package com.amoelcodigo.crud.security.service;

import com.amoelcodigo.crud.security.entity.Usuario;
import com.amoelcodigo.crud.security.repository.UsuarioRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.Optional;

@Service
@Transactional
public class UsuarioService {

    @Autowired
    UsuarioRepository usuarioRepository;

    public Optional<Usuario> getByUsuario(String nombreUsuario){
        return usuarioRepository.findByNombreUsuario(nombreUsuario);
    }

    public Boolean existsByUsuario(String nombreUsuario){
        return usuarioRepository.existsByNombreUsuario(nombreUsuario);
    }

    public Boolean existsByEmail(String email){
        return usuarioRepository.existsByEmail(email);
    }

    public void save(Usuario usuario){
        usuarioRepository.save(usuario);
    }


}
package com.amoelcodigo.crud.security.service;

import com.amoelcodigo.crud.security.entity.Usuario;
import com.amoelcodigo.crud.security.entity.UsuarioMain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

/**
 * Clase que convierte la clase usuario en un UsuarioMain
 * UserDetailsService es propia de Spring Security
 */
@Service
@Transactional
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UsuarioService usuarioService;

    @Override
    public UserDetails loadUserByUsername(String nombreUsuario) throws UsernameNotFoundException {
        Usuario usuario = usuarioService.getByUsuario(nombreUsuario).get();
        return UsuarioMain.build(usuario);
    }
}

Paquete jwt

package com.amoelcodigo.crud.security.jwt;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Comprueba si existe un token si no devuelve un 401 no autorizado
 */
@Component
public class JwtEntryPoint implements AuthenticationEntryPoint {

    // Implementamos un logger para ver cual metodo da error en caso de falla
    private final static Logger logger = LoggerFactory.getLogger(JwtEntryPoint.class);

  //Metodo implementado de AuthenticationEntryPoint
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        logger.error("Fallo el metodo commence");
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "No esta autorizado");
    }
}
package com.amoelcodigo.crud.security.jwt;

import com.amoelcodigo.crud.security.entity.UsuarioMain;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * Clase que genera el token y valida que este bien formado y no este expirado
 */
@Component
public class JwtProvider {

    // Implementamos un logger para ver cual metodo da error en caso de falla
    private final static Logger logger = LoggerFactory.getLogger(JwtProvider.class);

    //Valores que tenemos en el aplicattion.properties
    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private int expiration;

    /**
     *setIssuedAt --> Asigna fecha de creción del token
     *setExpiration --> Asigna fehca de expiración
     * signWith --> Firma
     */
    public String generateToken(Authentication authentication){
        UsuarioMain usuarioMain = (UsuarioMain) authentication.getPrincipal();
        return Jwts.builder().setSubject(usuarioMain.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(new Date().getTime() + expiration * 1000))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    //subject --> Nombre del usuario
    public String getNombreUsuarioFromToken(String token){
           return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
    }

    public Boolean validateToken(String token){
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        }catch (MalformedJwtException e){
            logger.error("Token mal formado");
        }catch (UnsupportedJwtException e){
            logger.error("Token no soportado");
        }catch (ExpiredJwtException e){
            logger.error("Token expirado");
        }catch (IllegalArgumentException e){
            logger.error("Token vacio");
        }catch (SignatureException e){
            logger.error("Fallo con la firma");
        }
        return false;
    }
}
package com.amoelcodigo.crud.security.jwt;

import com.amoelcodigo.crud.security.service.UserDetailsServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Se ejecuta por cada petición, comprueba que sea valido el token
 * Utiliza el provider para validar que sea valido
 * Si es valido permite acceso al recurso si no lanza una excepción
 */
public class JwtTokenFilter extends OncePerRequestFilter {

    private final static Logger logger = LoggerFactory.getLogger(JwtTokenFilter.class);

    @Autowired
    JwtProvider jwtProvider;

    @Autowired
    UserDetailsServiceImpl userDetailsService;

    // El token esta formado por:
     // cabecera --> Authorization: Bearer token
    //Hace las comprobaciones
    // Este metodo se hace cada vez que se le haga una peticion al sever
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        try{
            String token = getToken(request);

            if(token != null && jwtProvider.validateToken(token)){

                String nombreUsuario = jwtProvider.getNombreUsuarioFromToken(token);
                UserDetails userDetails = userDetailsService.loadUserByUsername(nombreUsuario);
                UsernamePasswordAuthenticationToken auth =
                        new UsernamePasswordAuthenticationToken(userDetails,
                                null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);

            }
        }catch (Exception e){
            logger.error("Fail en el método doFilter " + e.getMessage());
        }
        filterChain.doFilter(request, response);
    }


    //Obtenemos el token sin Bearer + el espacio
    private String getToken(HttpServletRequest request){

        String header = request.getHeader("Authorization");
        if(header != null && header.startsWith("Bearer"))
            return header.replace("Bearer ", "");
        return null;

    }
}

MainSecurity.java

package com.amoelcodigo.crud.security;

import com.amoelcodigo.crud.security.jwt.JwtEntryPoint;
import com.amoelcodigo.crud.security.jwt.JwtTokenFilter;
import com.amoelcodigo.crud.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
//con perPostEnabled se usa para indicar a q metodos puede acceder solo el admin
// Los metodos que no lleven anotación pueden acceder el admin como un generic user
// @preauthorized solo puede acceder el admin
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MainSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsServiceImpl userDetailsService;

    //Devuelve el mensaje de no autorizado
    @Autowired
    JwtEntryPoint jwtEntryPoint;

    @Bean
    public JwtTokenFilter jwtTokenFilter(){
        return new JwtTokenFilter();
    }

    /**
     * Encripta el pasword
     * @return pasword ecriptado
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //Desactivamos cookies ya que enviamos un token
            // cada vez que hacemos una petición
        http.cors().and().csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(jwtEntryPoint)
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}
  • .antMatchers(“/auth/**”).permitAll() : Le decimos que permitimos apuntar sin necesidad de mandar un toquen todo lo que este despues de la ruta auth/endpoint

Paquete DTO

package com.amoelcodigo.crud.security.dto;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import java.util.HashSet;
import java.util.Set;

public class NuevoUsuario {

    @NotBlank
    private String nombre;
    @NotBlank
    private String nombreUsuario;
    @Email
    private String email;
    @NotBlank
    private String password;
    //Por defecto crea un usuario normal
    //Si quiero un usuario Admin debo pasar este campo roles
    private Set<String> roles = new HashSet<>();

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public String getNombreUsuario() {
        return nombreUsuario;
    }

    public void setNombreUsuario(String nombreUsuario) {
        this.nombreUsuario = nombreUsuario;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Set<String> getRoles() {
        return roles;
    }

    public void setRoles(Set<String> roles) {
        this.roles = roles;
    }
}
package com.amoelcodigo.crud.security.dto;

import javax.validation.constraints.NotBlank;

public class LoginUsuario {

    @NotBlank
    private String nombreUsuario;
    @NotBlank
    private String password;

    public String getNombreUsuario() {
        return nombreUsuario;
    }

    public void setNombreUsuario(String nombreUsuario) {
        this.nombreUsuario = nombreUsuario;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
package com.amoelcodigo.crud.security.dto;

import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class JwtDto {

    private String token;
    private String bearer = "Bearer";
    private String nombreUsuario;
    private Collection<? extends GrantedAuthority> authorities;

    public JwtDto(String token, String nombreUsuario, Collection<? extends GrantedAuthority> authorities) {
        this.token = token;
        this.nombreUsuario = nombreUsuario;
        this.authorities = authorities;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String getBearer() {
        return bearer;
    }

    public void setBearer(String bearer) {
        this.bearer = bearer;
    }

    public String getNombreUsuario() {
        return nombreUsuario;
    }

    public void setNombreUsuario(String nombreUsuario) {
        this.nombreUsuario = nombreUsuario;
    }

    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }
}

Paquete Controller

package com.amoelcodigo.crud.security.controller;

import com.amoelcodigo.crud.dto.Mensaje;
import com.amoelcodigo.crud.security.dto.JwtDto;
import com.amoelcodigo.crud.security.dto.LoginUsuario;
import com.amoelcodigo.crud.security.dto.NuevoUsuario;
import com.amoelcodigo.crud.security.entity.Rol;
import com.amoelcodigo.crud.security.entity.Usuario;
import com.amoelcodigo.crud.security.enums.RolNombre;
import com.amoelcodigo.crud.security.jwt.JwtProvider;
import com.amoelcodigo.crud.security.service.RolService;
import com.amoelcodigo.crud.security.service.UsuarioService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.HashSet;
import java.util.Set;

@RestController
@RequestMapping("/auth")
@CrossOrigin
public class AuthController {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    UsuarioService usuarioService;

    @Autowired
    RolService rolService;

    @Autowired
    JwtProvider jwtProvider;

    //Espera un json y lo convierte a tipo clase NuevoUsuario
    @PostMapping("/nuevoUsuario")
    public ResponseEntity<?> nuevoUsuario(@Valid @RequestBody NuevoUsuario nuevoUsuario,
                                          BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return new ResponseEntity<>(new Mensaje("Campos mal o email invalido"), HttpStatus.BAD_REQUEST);
        }
        if(usuarioService.existsByUsuario(nuevoUsuario.getNombreUsuario())){
            return new ResponseEntity<>(new Mensaje("Ese nombre ya existe"), HttpStatus.BAD_REQUEST);
        }
        if(usuarioService.existsByEmail(nuevoUsuario.getEmail())){
            return new ResponseEntity<>(new Mensaje("Ese email ya existe"), HttpStatus.BAD_REQUEST);
        }

        Usuario usuario = new Usuario(nuevoUsuario.getNombre(), nuevoUsuario.getNombreUsuario(),
                nuevoUsuario.getEmail(), passwordEncoder.encode(nuevoUsuario.getPassword()));

        Set<Rol> roles = new HashSet<>();
        roles.add(rolService.getByRolNombre(RolNombre.ROLE_USER).get());
        if(nuevoUsuario.getRoles().contains("admin"))
            roles.add(rolService.getByRolNombre(RolNombre.ROLE_ADMIN).get());
        usuario.setRoles(roles);

        usuarioService.save(usuario);

        return new ResponseEntity<>(new Mensaje("Usuario creado"), HttpStatus.CREATED);
    }

    @PostMapping("/login")
    public ResponseEntity<JwtDto> login(@Valid @RequestBody LoginUsuario loginUsuario, BindingResult bindingResult){
        if (bindingResult.hasErrors())
            return new ResponseEntity(new Mensaje("Campos mal"), HttpStatus.BAD_REQUEST);
        Authentication authentication =
                authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(loginUsuario.getNombreUsuario(),
                                loginUsuario.getPassword()));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        String jwt = jwtProvider.generateToken(authentication);
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        JwtDto jwtDto = new JwtDto(jwt, userDetails.getUsername(), userDetails.getAuthorities());
        return new ResponseEntity<>(jwtDto, HttpStatus.OK);
    }
}

Paquete util

Con esta clase insertamos dos roles (Admin y User).

OBSERVACIÓN: Comentar clase después del primer run de la aplicación y verificar que se han creado los roles.

package com.amoelcodigo.crud.util;

import com.amoelcodigo.crud.security.entity.Rol;
import com.amoelcodigo.crud.security.enums.RolNombre;
import com.amoelcodigo.crud.security.service.RolService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

// Comentar o borrar clase despues del primer run de la aplicación
@Component
public class CreateRoles implements CommandLineRunner {

    @Autowired
    RolService rolService;

    @Override
    public void run(String... args) throws Exception {
        Rol rolAdmin = new Rol(RolNombre.ROLE_ADMIN);
        Rol rolUser = new Rol(RolNombre.ROLE_USER);
        rolService.save(rolAdmin);
        rolService.save(rolUser);
    }
} 

@PreAuthorize

Poner en Torre Controller @PreAuthorize(“hasRole(‘ADMIN’)”) a los métodos que quieran que solo pueda acceder el Rol admin

 @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/crearTorre")
    public ResponseEntity<?> creaTorre(@RequestBody TorreDto torreDto){
    }

BootRun

Al dar inicio a nuestra aplicación se van a crear las siguientes tablas en nuestra base de datos:

Y si miramos la tabla rol tiene los siguientes datos:

Probando en Postman

Creación de usuario.
Login exitoso de admin

El rol admin por defecto tiene también el rol User, nos devuelve un token que es que utilizamos para todas las peticiones.

Recordemos que para cada petición debemos mandar un token que nos devuelve nuestro servicio de login y debes ser de tipo Bearer.

Respuesta de servicio cuando el usuario no tiene el Rol correspondiente para hacer la acción.

Colección de Postman

En el siguiente link esta la colección de postman

Repositorio del proyecto

En el siguiente link esta el repositorio del proyecto.

Cualquier duda déjala en la caja de comentarios.

Contacto

One Reply to “Implementación de seguridad y roles con Spring Security, JWT y MySQL”

  1. Excelente tutorial, el mejor que he visto de Spring Boot hasta ahora, tengo una duda si quisiera poner el Bearer+token en el header del HttpServletResponse response del mencionado método, como podria hacerlo?

Leave a Reply

Your email address will not be published.