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.
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 attemptAuthentication
y successfulAuthentication
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.
Gracias, muy bueno
ResponderEliminar