spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
Solo para brindar algunos antecedentes, tenemos una maravillosa introducción, ¡cortesía de jwt.io ! Echemos un vistazo:
JSON Web Token (JWT) es un estándar abierto (RFC 7519) que define una forma compacta y autónoma de transmitir información de forma segura entre partes como un objeto JSON. Esta información se puede verificar y confiar porque está firmada digitalmente. Los JWT se pueden firmar mediante un secreto (con el algoritmo HMAC) o un par de claves pública/privada mediante RSA.
Expliquemos más algunos conceptos de esta definición.
Compacto : debido a su tamaño más pequeño, los JWT se pueden enviar a través de una URL, un parámetro POST o dentro de un encabezado HTTP. Además, el tamaño más pequeño significa que la transmisión es rápida.
Autocontenido : la carga útil contiene toda la información requerida sobre el usuario, evitando la necesidad de consultar la base de datos más de una vez.
A continuación se muestran algunos escenarios en los que los tokens web JSON son útiles:
Autenticación : este es el escenario más común para usar JWT. Una vez que el usuario haya iniciado sesión, cada solicitud posterior incluirá el JWT, lo que permitirá al usuario acceder a rutas, servicios y recursos permitidos con ese token. El inicio de sesión único es una característica que JWT utiliza ampliamente hoy en día, debido a su pequeña sobrecarga y su capacidad para usarse fácilmente en diferentes dominios.
Intercambio de información : los tokens web JSON son una buena forma de transmitir información de forma segura entre las partes. Dado que los JWT se pueden firmar (por ejemplo, utilizando pares de claves públicas y privadas), puede estar seguro de que los remitentes son quienes dicen ser. Además, como la firma se calcula utilizando el encabezado y la carga útil, también puedes verificar que el contenido no haya sido manipulado.
Los JSON Web Tokens constan de tres partes separadas por puntos (.) , que son:
Por lo tanto, un JWT suele tener el siguiente aspecto.
xxxxx
. yyyyy
. zzzzz
Analicemos las diferentes partes.
Encabezamiento
El encabezado normalmente consta de dos partes: el tipo de token, que es JWT, y el algoritmo hash que se utiliza, como HMAC SHA256 o RSA.
Por ejemplo:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
Luego, este JSON está codificado en Base64Url para formar la primera parte del JWT.
Carga útil
La segunda parte del token es la carga útil, que contiene los reclamos. Los reclamos son declaraciones sobre una entidad (normalmente, el usuario) y metadatos adicionales. Hay tres tipos de reclamaciones: reclamaciones reservadas, públicas y privadas.
Tenga en cuenta que los nombres de las reclamaciones tienen solo tres caracteres, ya que JWT debe ser compacto.
Reclamaciones públicas : pueden ser definidas a voluntad por quienes utilizan JWT. Pero para evitar colisiones, deben definirse en el Registro de tokens web JSON de IANA o definirse como un URI que contenga un espacio de nombres resistente a colisiones.
Reclamos privados : Son los reclamos personalizados creados para compartir información entre partes que acuerdan utilizarlos.
Un ejemplo de carga útil podría ser:
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
Luego, la carga útil se codifica en Base64Url para formar la segunda parte del token web JSON.
Firma
Para crear la parte de la firma, debe tomar el encabezado codificado, la carga útil codificada, un secreto, el algoritmo especificado en el encabezado y firmarlo.
Por ejemplo si desea utilizar el algoritmo HMAC SHA256, la firma se creará de la siguiente manera:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
La firma se utiliza para verificar que el remitente del JWT es quien dice ser y para garantizar que el mensaje no haya cambiado en el camino. Poniendo todo junto
El resultado son tres cadenas Base64 separadas por puntos que se pueden pasar fácilmente en entornos HTML y HTTP, a la vez que son más compactas en comparación con estándares basados en XML como SAML.
A continuación se muestra un JWT que tiene el encabezado y la carga útil anteriores codificados y está firmado con un secreto. JWT codificado
En la autenticación, cuando el usuario inicia sesión exitosamente usando sus credenciales, se devolverá un token web JSON y se deberá guardar localmente (generalmente en el almacenamiento local, pero también se pueden usar cookies), en lugar del enfoque tradicional de crear una sesión en el servidor y devolver una cookie.
Siempre que el usuario desee acceder a una ruta o recurso protegido, el agente de usuario debe enviar el JWT, normalmente en el encabezado Autorización utilizando el esquema Portador. El contenido del encabezado debería verse como el siguiente:
Authorization: Bearer <token>
Este es un mecanismo de autenticación sin estado ya que el estado del usuario nunca se guarda en la memoria del servidor. Las rutas protegidas del servidor buscarán un JWT válido en el encabezado de Autorización y, si está presente, el usuario podrá acceder a los recursos protegidos. Como los JWT son autónomos, toda la información necesaria está ahí, lo que reduce la necesidad de consultar la base de datos varias veces.
Esto le permite confiar plenamente en las API de datos que no tienen estado e incluso realizar solicitudes a servicios posteriores. No importa qué dominios estén sirviendo a sus API, por lo que el intercambio de recursos entre orígenes (CORS) no será un problema ya que no utiliza cookies.
El siguiente diagrama muestra este proceso:
Los esquemas de autenticación basados en tokens se han vuelto inmensamente populares en los últimos tiempos, ya que brindan importantes beneficios en comparación con las sesiones/cookies:
Es necesario hacer algunas concesiones con este enfoque:
El flujo de autenticación JWT es muy simple
Es importante tener en cuenta que los reclamos de autorización se incluirán con el token de acceso. ¿Por qué es esto importante? Bueno, digamos que los reclamos de autorización (por ejemplo, privilegios de usuario en la base de datos) se cambian durante la vida útil del token de acceso. Esos cambios no entrarán en vigor hasta que se emita un nuevo token de acceso. En la mayoría de los casos, esto no es un gran problema, porque los tokens de acceso son de corta duración. De lo contrario, opte por el patrón de token opaco.
Veamos cómo podemos implementar la autenticación basada en token JWT usando Java y Spring, mientras intentamos reutilizar el comportamiento predeterminado de seguridad de Spring donde podamos. El marco Spring Security viene con clases de complementos que ya se ocupan de mecanismos de autorización como: cookies de sesión, HTTP Basic y HTTP Digest. Sin embargo, carece de soporte nativo para JWT y debemos ensuciarnos las manos para que funcione.
Esta demostración utiliza actualmente una base de datos H2 llamada test_db para que pueda ejecutarla rápidamente y lista para usar sin mucha configuración. Si desea conectarse a una base de datos diferente, debe especificar la conexión en el archivo application.yml
dentro del directorio de recursos. Tenga en cuenta que hibernate.hbm2ddl.auto=create-drop
se eliminará y creará una base de datos limpia cada vez que implementemos (es posible que desee cambiarla si está usando esto en un proyecto real). Aquí está el ejemplo del proyecto, vea con qué facilidad puede intercambiar comentarios en la url
y las propiedades dialect
para usar su propia base de datos MySQL:
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtTokenFilter
El filtro JwtTokenFilter
se aplica a cada API ( /**
) con excepción del punto final del token de inicio de sesión ( /users/signin
) y el punto final de registro ( /users/signup
).
Este filtro tiene las siguientes responsabilidades:
JwtTokenProvider
de lo contrario, generará una excepción de autenticación Asegúrese de que chain.doFilter(request, response)
se invoque tras una autenticación exitosa. Quiere que el procesamiento de la solicitud avance al siguiente filtro, porque el último filtro FilterSecurityInterceptor#doFilter es responsable de invocar el método en su controlador que maneja el recurso API solicitado.
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
Configurador de JwtTokenFilter
Agrega JwtTokenFilter
a DefaultSecurityFilterChain
de Spring Boot Security.
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
JwtTokenProvider
El JwtTokenProvider
tiene las siguientes responsabilidades:
MisDetallesdeUsuario
Implementa UserDetailsService
para definir nuestra propia función loadUserbyUsername personalizada. La interfaz UserDetailsService
se utiliza para recuperar datos relacionados con el usuario. Tiene un método llamado loadUserByUsername que encuentra una entidad de usuario basada en el nombre de usuario y puede anularse para personalizar el proceso de búsqueda del usuario.
Lo utiliza DaoAuthenticationProvider
para cargar detalles sobre el usuario durante la autenticación.
Configuración de seguridad web
La clase WebSecurityConfig
amplía WebSecurityConfigurerAdapter
para proporcionar una configuración de seguridad personalizada.
Los siguientes beans se configuran y crean instancias en esta clase:
JwtTokenFilter
PasswordEncoder
Además, dentro del método WebSecurityConfig#configure(HttpSecurity http)
configuraremos patrones para definir puntos finales de API protegidos/desprotegidos. Tenga en cuenta que hemos desactivado la protección CSRF porque no utilizamos cookies.
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
Asegúrate de tener Java 8 y Maven instalados
Bifurca este repositorio y clonalo
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
en su navegador para verificar que todo esté funcionando correctamente. Puede cambiar el puerto predeterminado en el archivo application.yml
server :
port : 8080
/users/me
para comprobar que no está autenticado. Debería recibir una respuesta con un 403
con un mensaje Access Denied
ya que aún no ha configurado su token JWT válido. $ curl -X GET http://localhost:8080/users/me
/users/signin
con el usuario administrador predeterminado que creamos programáticamente para obtener un token JWT válido $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
nuevamente $ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}