Creando un REST Service en Spring Boot con autenticación básica JWT | Java

Actualmente los REST Services estan siendo bastante utilizados para manejar datos entre el servidor y el cliente, es decir entre el Backend y el Frontend, por tal motivo vamos a crear un servicio REST con una autenticación basada en Tokens utilizando la libreria JWT (JSON Web Token). Para iniciar el proyecto asegúrate de tener todas las librerías necesarias.
Spring Boot REST API
Archivo build.gradle

Modelos y entidades

Primero crearemos nuestros modelos en este caso User y Example, el primero va a ser una entidad de la base de datos y obtener el usuario que esta intentando iniciar sesión, el segundo es una clase simple que contendra una variable Long y String con sus respectivos getters y su constructor.
import java.util.Collection;

import  javax.persistence.Column;
import  javax.persistence.Entity;
import  javax.persistence.GeneratedValue;
import  javax.persistence.GenerationType;
import  javax.persistence.Id;
import  javax.persistence.Table;

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

@Entity
@Table(name = "users")
public class User implements UserDetails {
    private static final long serialVersionUID = -6177815612965306037L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "username", unique = true, nullable = false, length = 20)
    private String username;

    @Column(name = "password", nullable = false, length = 128)
    private String password;

    public User() {

    }

    public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
    }

    @Override
    public CollectionCollection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

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

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

    @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 class Example {
    private final Long id;
    private final String text;
	
    public Example(Long id, String text) {
        this.id=id;
        this.text=text;
    }

    public Long getId() {
        return id;
    }

    public String getText() {
        return text;
    }
}
Como se observa son modelos bastante simples, es decir son clases con sus respectivos Getters, contructores y representa los datos que maneja el sistema los mismos que contendrán los datos de usuario y una clase ejemplo que mostrara un texto y un id solamente al usuario autorizado.
Video tutorial completo de este proyecto.

Interface repositorio

import org.springframework.data.jpa.repository.JpaRepository;

import com.alexastudillo.tutorialrest.model.User;

public interface UserRepository extends JpaRepository<User, Long>{
    User findByUsername(String username);
}
En este caso hemos creado un repositorio de la clase User y se agregó el método para que busque el usuario en la base de datos por el nombre, como sabemos un repositorio en Spring posee los métodos necesarios de un CRUD, y en este caso le agregamos una más que es findByUsername, este es el único repositorio que usaremos para autenticar al usuario.

Controlador

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.alexastudillo.tutorialrest.model.Example;

@RestController
public class ExampleController {
    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();
	
    @GetMapping("/example")
    public Example example(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Example(counter.incrementAndGet(), String.format(template, name));
    }
}
Vamos a crear un único controlador que será un RestController de la clase Example como se observa tenemos un String que se le pasará a la clase Example como texto y un AtomicCounter que lo enviaremos como el id; en el método Get simplemente retornamos al cliente los datos de la clase que Spring lo envia en forma de JSON y tendremos una cadena de la forma {"id": 1, "text": "Hello World!"}.

Servicio

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 org.springframework.transaction.annotation.Transactional;

import com.alexastudillo.tutorialrest.repository.UserRepository;

@Service
public class UserDetailsServiceImpl implements UserDetailsService{
	
    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username);
    }
}
Se utilizará un único servicio, como se observa implementamos la interface UserDetailsService del paquete de Spring Security, declaramos nuestro repositorio y sobre-escribimos el método loadUserByUsername, para poder buscar el usuario que está intentando ingresar al sistema.

Filtros

Autenticación

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.alexastudillo.tutorialrest.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException {
        String token = Jwts.builder().setIssuedAt(new Date()).setIssuer("https://www.alexastudillo.com")
            .setSubject((((User) authResult.getPrincipal()).getUsername()))
            .setExpiration(new Date(System.currentTimeMillis() + 864_000_000))
            .signWith(SignatureAlgorithm.HS512, "alex1234").compact();
        response.addHeader("Authorization", "Bearer " + token);
    }
	
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException failed) throws IOException, ServletException {
        super.unsuccessfulAuthentication(request, response, failed);
    }
}
Nuestro primer filtro es de autenticación, sobre-escribimos los métodos attemptAuthenticationsuccessfulAuthentication el primero gestiona cuando el cliente envia el usuario y contraseña, validando en la base de datos, el segundo método otorga el Token en el header para que el ciente los almacene, en este caso hemos configurado un token con 10 días de validez, el último método es en caso de que las credenciales de usuario no sean correctas o si ocurrió algún error al autenticar al usuario, pero en este caso al ser un ejemplo vamos a enviar datos correctos y no existirá un fallo en la autenticación, pero no te olvides de gestionar el método cuando este en producción.

Autorización

import java.io.IOException;
import java.util.ArrayList;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import io.jsonwebtoken.Jwts;

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer")) {
            chain.doFilter(request, response);
            return;
        }
        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if (token != null) {
            String user = Jwts.parser().setSigningKey("alex1234").parseClaimsJws(token.replace("Bearer", "")).getBody()
                .getSubject();
            if (user != null)
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
        }
        return null;
    }
}
Este filtro permite que solo personas autorizadas obtengan los datos del sistema, aquí lo que hacemos es verificar el token que nos envia el usuario en la cabecera, lo validamos y damos el acceso, caso contrario el cliente obtendrá un error 403 de no autorizado.

Seguridad Web

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.password.NoOpPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
	
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
	
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().cors().and().csrf()
            .disable().authorizeRequests().antMatchers(HttpMethod.POST, "/login").permitAll().anyRequest()
            .authenticated().and().addFilter(new JWTAuthenticationFilter(authenticationManager()))
            .addFilter(new JWTAuthorizationFilter(authenticationManager())).headers();
    }
	
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}
Esta es la última clase que debemos configurar para dar acceso solo a usuarios autenticados, en el primer método usamos el servicio que hemos creado, ademas decimos que la contraseña es texto plano con NoOpPasswordEncoder actualmente marcado como @Deprecated, no se lo debe usar ya que siempre debemos encriptar las contraseñas en nuestra base de datos pero para el ejemplo utilizamos texto plano.

En el segundo método nos encargamos de marcar la sessión como STATELESS para que no se cree ninguna sesión ni utilice una existente, ya que estamos manejando tokens, como paso siguiente habilitamos CORS (Intercambio de recursos de origen cruzado) para poder acceder a los datos desde cualquier cliente, luego deshabilitamos CSRF (Falsificación de petición en sitios cruzados), en este caso lo deshabilitamos para que el método POST sea más fácil de ejecutar pero siempre deberíamos tenerlo habilitado.

Finalmente otorgamos permiso del tipo POST a todos los usarios a /login, pero también se aplica los filtros para las demás direcciones, dando como resultado que solo los usuarios autenticados puedan acceder a otras ubicaciones, también puedes trabajar con roles para dar acceso a diferentes lugares de tu sistema, pero para el ejemplo hacemos que todos los usuarios autenticados tengan acceso.

Comentarios

Publicar un comentario

Entradas populares