142. Spring Security¶
Ejemplo en el proyecto Requisitos
142.1 Introducción¶
Hay que diferenciar entre autenticación y autorización
142.2 Mecanismos de autenticación¶
-
Username and Password - how to authenticate with a username/password
-
OAuth 2.0 Login - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub)
-
SAML 2.0 Login - SAML 2.0 Log In
-
Central Authentication Server (CAS) - Central Authentication Server (CAS) Support
-
Remember Me - how to remember a user past session expiration
-
JAAS Authentication - authenticate with JAAS
-
Pre-Authentication Scenarios - authenticate with an external mechanism such as SiteMinder or Java EE security but still use Spring Security for authorization and protection against common exploits.
-
X509 Authentication - X509 Authentication
142.3 Autenticación y autorización¶
Elementos que confirman Spring Security:
-
Filtros: piensa en los filtros de seguridad como puntos de control en diferentes puertas de un edificio. Cuando llega una solicitud, pasa por estos filtros. Estos filtros manejan tareas como la autenticación y autorización. Por ejemplo, podría haber un filtro que verifica si tienes una tarjeta de acceso adecuada (autenticación) y otro que asegura que puedas entrar a habitaciones específicas (autorización).
-
Autenticación: cuando inicias sesión, Spring Security verifica tus credenciales (como nombre de usuario y contraseña). Si coinciden, estás autenticado. Spring Security utiliza proveedores de autenticación, que pueden ser una base de datos, LDAP o cualquier otra fuente, para verificar tu identidad.
-
Contexto de Seguridad: una vez autenticado, tus detalles de seguridad se almacenan en el Contexto de Seguridad. Es como recibir un pase especial después de pasar por el punto de control. Este pase (contexto de seguridad) contiene tus roles y permisos.
-
Autorización: ahora, cuando intentas acceder a una parte específica de la aplicación, Spring Security verifica tus roles y permisos almacenados en el contexto de seguridad. Si estás autorizado (basado en tus roles), se te permite el acceso. De lo contrario, podrías ser denegado.
-
Personalización: Spring Security te permite personalizar los filtros de seguridad, proveedores de autenticación y reglas de acceso según los requisitos de tu aplicación. Puedes configurar qué URL necesitan autenticación, qué roles son requeridos, etc.
142.4 Seguridad en REST Api¶
Spring Security provides various mecanismos to secure our REST APIs. One of them is API keys. An API key is a token that a client provides when invoking API calls.
Sprint Security facilita varios mecanismos para asegurar los REST API. Uno de ellos es API key
.
Puesto que REST Api es sin estado debemos utilizar una sesión o cookies.
Instead, these should be secure using Basic authentication, API Keys, JWT, or OAuth2-based tokens.
142.4.1 OAuth2¶
OAuth2 es el estándar de facto para la seguridad REST APIs. Es un estándar de autenticación y autorización que permite a los propietarios de los recursos dar permisos a los clientes via access token
142.4.2 API Keys¶
Un Api key
es un token que identifica el cliente del API sin referenciar al cliente actual. El Api key puede enviarlo el cliente dentro de la query string o en la cabeera de la petición. Usando SSL podemos ocultar el token
142.4.3 Usando JWT Token (JSON Web Tokens)¶
https://codersee.com/spring-boot-3-spring-security-6-with-kotlin-jwt/
Elementos de Spring Security
-
Filters. Son como los puntos de chequeo en cada puerta del edificio. Cuando llegan las peticiones pasan por estos filtros. Estos filtros realizan tareas de autenticación y autorización
-
Autenticación . En el login Spring security comprueba las credenciales (ej usuario y clave). Spring Security usa proveedores de autenticación como bases de datos, LDAP o calquier otra fuente.
-
Security Context. Una vez autenticado , los detalles de seguridad se guardan en un
Security Context
. Es como un pase de seguridad para el resto de las zonas. Security Context contiene los "roles" y permisos -
Autorización. Cuando intetas entrar en partes concretas de la aplicación, Spring Security verifica los presmisos y roles almacenados en el contexto de seguridad.
-
Adaptación Spring Security permite personalizar los filtros, proveedores de autenticacion, y rglas de acceso . Puedes configurar que url necesita o no autenticatcion y que roles precisa.
142.4.4 Ejemplo de configuración detallada de seguridad de la web "codersee"¶
Partimos de un proyecto con Entidades, Service, REpository y Controlador de Rest Api.
142.4.4.1 Añadimos el modelo de usuario¶
User.kt:
import java.util.*
data class User(
val id: UUID,
val email: String,
val password: String,
val role: Role
)
enum class Role {
USER, ADMIN
}
User repository:
import com.codersee.jwtauth.model.Role
import com.codersee.jwtauth.model.User
import org.springframework.stereotype.Repository
import java.util.*
@Repository
class UserRepository {
private val users = mutableSetOf(
User(
id = UUID.randomUUID(),
email = "email-1@gmail.com",
password = "pass1",
role = Role.USER,
),
User(
id = UUID.randomUUID(),
email = "email-2@gmail.com",
password = "pass2",
role = Role.ADMIN,
),
User(
id = UUID.randomUUID(),
email = "email-3@gmail.com",
password = "pass3",
role = Role.USER,
),
)
fun save(user: User): Boolean =
users.add(user)
fun findByEmail(email: String): User? =
users
.firstOrNull { it.email == email }
fun findAll(): Set<User> =
users
fun findByUUID(uuid: UUID): User? =
users
.firstOrNull { it.id == uuid }
fun deleteByUUID(uuid: UUID): Boolean {
val foundUser = findByUUID(uuid)
return foundUser?.let {
users.removeIf {
it.id == uuid
}
} ?: false
}
}
User service
import com.codersee.jwtauth.model.User
import com.codersee.jwtauth.repository.UserRepository
import org.springframework.stereotype.Service
import java.util.*
@Service
class UserService(
private val userRepository: UserRepository
) {
fun createUser(user: User): User? {
val found = userRepository.findByEmail(user.email)
return if (found == null) {
userRepository.save(user)
user
} else null
}
fun findByUUID(uuid: UUID): User? =
userRepository.findByUUID(uuid)
fun findAll(): List<User> =
userRepository.findAll()
.toList()
fun deleteByUUID(uuid: UUID): Boolean =
userRepository.deleteByUUID(uuid)
}
User Rest Api
data class UserRequest(
val email: String,
val password: String,
)
El UserDTO para la respuesta:
import java.util.*
data class UserDTO(
val uuid: UUID,
val email: String,
)
El UserController
import com.codersee.jwtauth.model.Role
import com.codersee.jwtauth.model.User
import com.codersee.jwtauth.service.UserService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ResponseStatusException
import java.util.*
@RestController
@RequestMapping("/api/user")
class UserController(
private val userService: UserService
) {
@PostMapping
fun create(@RequestBody userRequest: UserRequest): UserDTO =
userService.createUser(userRequest.toModel())
?.toResponse()
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create user.")
@GetMapping
fun listAll(): List<UserDTO> =
userService.findAll()
.map { it.toResponse() }
@GetMapping("/{uuid}")
fun findByUUID(@PathVariable uuid: UUID): UserDTO =
userService.findByUUID(uuid)
?.toResponse()
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found.")
@DeleteMapping("/{uuid}")
fun deleteByUUID(@PathVariable uuid: UUID): ResponseEntity<Boolean> {
val success = userService.deleteByUUID(uuid)
return if (success)
ResponseEntity.noContent()
.build()
else
throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found.")
}
private fun User.toResponse(): UserDTO =
UserDTO(
uuid = this.id,
email = this.email,
)
private fun UserRequest.toModel(): User =
User(
id = UUID.randomUUID(),
email = this.email,
password = this.password,
role = Role.USER,
)
}
As we can see, the logic above is pretty similar to what we’ve done before.
We mark our class with @RestController and @RequestMapping, functions with annotations matching HTTP Methods they are gonna handle, and also our path variables and request bodies.
When converting between models/requests/responses, we make use of extension functions, which are my favorite approach for mappers in Kotlin.
Lastly, whenever we want to return anything else than 200 OK, we use the ResponseStatusException, which is a clean way to do so.
142.4.4.2 Incluimos dependencias de Spring Security¶
dependencies {
// other imports
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
implementation("io.jsonwebtoken:jjwt-impl:0.12.3")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.3")
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security:spring-security-test")
}
Despues de sincronizar y ejecutar en el log aparece:
Using generated security password: 52d7d5b8-ce49-4d93-a4e1-721780290e58
This generated password is for development use only. Your security configuration must be updated before running your application in production.
Al añadir las dependencias se activa por defecto la seguridad básica con usuario y clave.
El usuario inicial es user
y la clave la generada en el log.
142.4.4.3 Creamos JWT token¶
Editamos application.yaml
jwt:
key: ${JWT_KEY}
access-token-expiration: 3600000
refresh-token-expiration: 86400000
la key
es una variable de entorno. No debe incluirse aquí.
Usamos @ConfigurationProperties
Añadimos configuration package y la clase JwtProperties :
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("jwt")
data class JwtProperties(
val key: String,
val accessTokenExpiration: Long,
val refreshTokenExpiration: Long,
)
Añadimos la clase Configuration
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration
@Configuration
@EnableConfigurationProperties(JwtProperties::class)
class Configuration
@Service
class TokenService(
jwtProperties: JwtProperties
) {
private val secretKey = Keys.hmacShaKeyFor(
jwtProperties.key.toByteArray()
)
fun generate(
userDetails: UserDetails,
expirationDate: Date,
additionalClaims: Map<String, Any> = emptyMap()
): String =
Jwts.builder()
.claims()
.subject(userDetails.username)
.issuedAt(Date(System.currentTimeMillis()))
.expiration(expirationDate)
.add(additionalClaims)
.and()
.signWith(secretKey)
.compact()
fun isValid(token: String, userDetails: UserDetails): Boolean {
val email = extractEmail(token)
return userDetails.username == email && !isExpired(token)
}
fun extractEmail(token: String): String? =
getAllClaims(token)
.subject
fun isExpired(token: String): Boolean =
getAllClaims(token)
.expiration
.before(Date(System.currentTimeMillis()))
private fun getAllClaims(token: String): Claims {
val parser = Jwts.parser()
.verifyWith(secretKey)
.build()
return parser
.parseSignedClaims(token)
.payload
}
}
secretKey es una instancia de Key que nos sirve para firmar y verificar los token JWT. Usamos el
algorítmo HMAC-SHA
private val secretKey = Keys.hmacShaKeyFor(
jwtProperties.key.toByteArray()
)
La función generate
crea y serializa URL-safe string con el token JWT. Incluimos el "subject" , fecha expiración y resto de argumentos pasados a la función.
fun generate(
userDetails: UserDetails,
expirationDate: Date,
additionalClaims: Map<String, Any> = emptyMap()
): String =
Jwts.builder()
.claims()
.subject(userDetails.username)
.issuedAt(Date(System.currentTimeMillis()))
.expiration(expirationDate)
.add(additionalClaims)
.and()
.signWith(secretKey)
.compact()
El resto de las funciones nos permiten extraer valores de los tokens y validarlos.
142.4.4.4 Implementamos Custom UserDetailsService¶
UserDetails representa la información principal del usuario. En nuestro caso, utilizaremos la implementación predeterminada llamada User, que contiene la información por defecto, como el nombre de usuario, la contraseña y una colección de autoridades concedidas (roles).
El UserDetailsService no es más que una interfaz utilizada para cargar datos específicos del usuario. Es utilizada por Spring Security para interactuar con nuestra fuente de datos y validar a los usuarios durante la autenticación.
import com.codersee.jwtauth.repository.UserRepository
import org.springframework.security.core.userdetails.User
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
typealias ApplicationUser = com.codersee.jwtauth.model.User
@Service
class CustomUserDetailsService(
private val userRepository: UserRepository
) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails =
userRepository.findByEmail(username)
?.mapToUserDetails()
?: throw UsernameNotFoundException("Not found!")
private fun ApplicationUser.mapToUserDetails(): UserDetails =
User.builder()
.username(this.email)
.password(this.password)
.roles(this.role.name)
.build()
}
Actualizamo Configuration
import com.codersee.jwtauth.repository.UserRepository
import com.codersee.jwtauth.service.CustomUserDetailsService
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
@Configuration
@EnableConfigurationProperties(JwtProperties::class)
class Configuration {
@Bean
fun userDetailsService(userRepository: UserRepository): UserDetailsService =
CustomUserDetailsService(userRepository)
@Bean
fun encoder(): PasswordEncoder = BCryptPasswordEncoder()
@Bean
fun authenticationProvider(userRepository: UserRepository): AuthenticationProvider =
DaoAuthenticationProvider()
.also {
it.setUserDetailsService(userDetailsService(userRepository))
it.setPasswordEncoder(encoder())
}
@Bean
fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
config.authenticationManager
}
Primero registramos el @Bean
de tipo UserDetailsService
A continuación registramos PasswordEncoder , no debemos guardar las claves en texto sin codificar (encrypted o hashed) . Usamos BCrypt
Por último proporcionamos el AuthenticationProvider y lo configuramos con UserDetailsService
Actualizamos UserRepository
Para almacenar la clave de modo encriptado.
@Repository
class UserRepository(
private val encoder: PasswordEncoder
) {
private val users = mutableSetOf(
User(
id = UUID.randomUUID(),
email = "email-1@gmail.com",
password = encoder.encode("pass1"),
role = Role.USER,
),
User(
id = UUID.randomUUID(),
email = "email-2@gmail.com",
password = encoder.encode("pass2"),
role = Role.ADMIN,
),
User(
id = UUID.randomUUID(),
email = "email-3@gmail.com",
password = encoder.encode("pass3"),
role = Role.USER,
),
)
fun save(user: User): Boolean {
val updated = user.copy(password = encoder.encode(user.password))
return users.add(updated)
}
}
Implementamos AuthenticationFilter
import com.codersee.jwtauth.service.CustomUserDetailsService
import com.codersee.jwtauth.service.TokenService
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
@Component
class JwtAuthenticationFilter(
private val userDetailsService: CustomUserDetailsService,
private val tokenService: TokenService,
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val authHeader: String? = request.getHeader("Authorization")
if (authHeader.doesNotContainBearerToken()) {
filterChain.doFilter(request, response)
return
}
val jwtToken = authHeader!!.extractTokenValue()
val email = tokenService.extractEmail(jwtToken)
if (email != null && SecurityContextHolder.getContext().authentication == null) {
val foundUser = userDetailsService.loadUserByUsername(email)
if (tokenService.isValid(jwtToken, foundUser))
updateContext(foundUser, request)
filterChain.doFilter(request, response)
}
}
private fun String?.doesNotContainBearerToken() =
this == null || !this.startsWith("Bearer ")
private fun String.extractTokenValue() =
this.substringAfter("Bearer ")
private fun updateContext(foundUser: UserDetails, request: HttpServletRequest) {
val authToken = UsernamePasswordAuthenticationToken(foundUser, null, foundUser.authorities)
authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = authToken
}
}
Explicaciones.
Pero antes de eso, vamos a entender qué son los Filtros en el Framework de Spring.
Cada solicitud hecha a nuestra aplicación del Framework de Spring pasa a través de la cadena de filtros, donde cada filtro en la cadena puede realizar algunas operaciones sobre la solicitud o la respuesta.
En nuestro caso, queremos usar esta característica para autenticar solicitudes hechas a nuestra API REST. Queremos verificar si un usuario envió un token JWT y validar dicho token, y si todo está bien, queremos actualizar el Contexto de Seguridad de Spring.
Y eso es exactamente lo que está sucediendo en el código anterior.
Primero, verificamos si la solicitud contiene un encabezado de Autorización. Si no, no procedemos con esta función y simplemente pasamos la solicitud a lo largo de la cadena de filtros.
Luego, extraemos el token JWT. Un valor de encabezado de Autorización válido consiste en el Bearer
A continuación, leemos el valor del** correo electrónico del token**. Y cuando nos aseguramos de que no es nulo y no hay ningún principal previamente autenticado en el contexto de seguridad, obtenemos los UserDetails, que luego usamos para validarlos con el token.
Por último, simplemente actualizamos el contexto de seguridad en nuestro sistema con foundUser (que es UserDetails) y autoridades (que, en nuestro caso, serán ADMIN o USER).
En resumen, esta función será invocada una vez por cada solicitud. En términos simples, verificará el token JWT y si todo está bien, actualizará el contexto de Seguridad de Spring con información sobre el usuario y sus roles.
142.4.4.5 Autorización¶
Add Security Configuration
Llega la parte de autorización
Necesitamos una clase nueva - SecurityConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
@Configuration
@EnableWebSecurity
class SecurityConfiguration(
private val authenticationProvider: AuthenticationProvider
) {
@Bean
fun securityFilterChain(
http: HttpSecurity,
jwtAuthenticationFilter: JwtAuthenticationFilter
): DefaultSecurityFilterChain {
http
.csrf { it.disable() }
.authorizeHttpRequests {
it
.requestMatchers("/api/auth", "api/auth/refresh", "/error")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/user")
.permitAll()
.requestMatchers("/api/user**")
.hasRole("ADMIN")
.anyRequest()
.fullyAuthenticated()
}
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
return http.build()
}
}
Para decirlo de manera simple, esta es la forma en que podemos modificar la cadena predeterminada agregando, eliminando o reemplazando filtros para adaptar la configuración de seguridad según nuestras necesidades.
Lo primero que hacemos es deshabilitar la protección CSRF. Está habilitada por defecto y en la mayoría de los casos no es necesaria (pero en proyectos reales, por favor, aprenda un poco más sobre los ataques CSRF y si su base de código es vulnerable).
A continuación, configuramos la autorización. Podemos ver claramente que las solicitudes a “/api/auth”, “api/auth/refresh”, “/error” y las solicitudes POST a “/api/user” serán accesibles sin un token. Y eso es correcto: no podemos requerir un token de acceso válido para usuarios que quieren iniciar sesión, que quieren crear una nueva cuenta y cuando el cliente de la API quiere refrescar el token.
Y si te estás preguntando por qué el “/error” también está en esta lista, aquí viene la respuesta. Es debido a la forma en que Spring maneja los errores internamente. Sin eso, cada excepción que lancemos en nuestra base de código devolverá 403 Prohibido, en lugar del código de estado HTTP que proporcionamos.
En cuanto al resto de las solicitudes de “/api/user”: queremos permitir solo a los usuarios con rol ADMIN.
¿Y qué pasa con el resto de las solicitudes? Bueno, queremos que solo sean accedidas por usuarios completamente autenticados. Pero, ¿qué significa eso? En nuestra API REST sin estado, significa simplemente que cada usuario con un token JWT válido podrá acceder a ellas (independientemente de su rol).
Después de eso, informamos a Spring que nunca debe crear una HttpSession (queremos que nuestra seguridad sea sin estado) y que nunca debe usarla para obtener el SecurityContext.
Y por último, registramos nuestro CustomAuthenticationProvider y dejamos saber a Spring Security que JwtAuthenticationFilter que implementamos anteriormente debería agregarse antes del filtro UsernamePasswordAuthenticationFilter predeterminado (el que requería que especificáramos autenticación básica).
Expose Login Endpoint – Generate Access Token
Let’s create the config.auth package and add a new controller class:
import com.codersee.jwtauth.service.AuthenticationService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/auth")
class AuthController(
private val authenticationService: AuthenticationService
) {
@PostMapping
fun authenticate(
@RequestBody authRequest: AuthenticationRequest
): AuthenticationResponse =
authenticationService.authentication(authRequest)
}
data class AuthenticationRequest(
val email: String,
val password: String,
)
data class AuthenticationDTO(
val accessToken: String,
)
import com.codersee.jwtauth.controller.auth.AuthenticationRequest
import com.codersee.jwtauth.controller.auth.AuthenticationResponse
import com.codersee.jwtauth.controller.config.JwtProperties
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service
import java.util.*
@Service
class AuthenticationService(
private val authManager: AuthenticationManager,
private val userDetailsService: CustomUserDetailsService,
private val tokenService: TokenService,
private val jwtProperties: JwtProperties,
) {
fun authentication(authenticationRequest: AuthenticationRequest): AuthenticationResponse {
authManager.authenticate(
UsernamePasswordAuthenticationToken(
authenticationRequest.email,
authenticationRequest.password
)
)
val user = userDetailsService.loadUserByUsername(authenticationRequest.email)
val accessToken = createAccessToken(user)
return AuthenticationResponse(
accessToken = accessToken,
)
}
private fun createAccessToken(user: UserDetails) = tokenService.generate(
userDetails = user,
expirationDate = getAccessTokenExpiration()
)
private fun getAccessTokenExpiration(): Date =
Date(System.currentTimeMillis() + jwtProperties.accessTokenExpiration)
}
Si los valores de correo electrónico y contraseña no coinciden con ninguno de los usuarios en nuestro sistema, el método authenticate lanzará AuthenticationException y la API devolverá 403 Prohibido.
Por otro lado, cuando coinciden, obtendremos los UserDetails y lo usaremos para generar un token JWT con un tiempo de expiración establecido en application.yaml
En este momento, te animo encarecidamente a que ejecutes nuestra aplicación y trates de llamar a endpoints con diferentes casos. Si te gustaría usar una colección de Postman lista para usar, entonces puedes encontrar una con todos los endpoints en el próximo capítulo sobre tokens de actualización.
Introducimos un nuevo endpoint
data class TokenDTO(
val token: String
)
data class AuthenticationResponse(
val accessToken: String,
val refreshToken: String,
)
data class RefreshTokenRequest(
val token: String
)
@PostMapping("/refresh")
fun refreshAccessToken(
@RequestBody request: RefreshTokenRequest
): TokenResponse =
authenticationService.refreshAccessToken(request.token)
?.mapToTokenResponse()
?: throw ResponseStatusException(HttpStatus.FORBIDDEN, "Invalid refresh token.")
private fun String.mapToTokenResponse(): TokenResponse =
TokenResponse(
token = this
)
**Refresh token repository^^
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Component
@Component
class RefreshTokenRepository {
private val tokens = mutableMapOf<String, UserDetails>()
fun findUserDetailsByToken(token: String) : UserDetails? =
tokens[token]
fun save(token: String, userDetails: UserDetails) {
tokens[token] = userDetails
}
}
import com.codersee.jwtauth.controller.auth.AuthenticationRequest
import com.codersee.jwtauth.controller.auth.AuthenticationResponse
import com.codersee.jwtauth.controller.config.JwtProperties
import com.codersee.jwtauth.repository.RefreshTokenRepository
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service
import java.util.*
@Service
class AuthenticationService(
private val authManager: AuthenticationManager,
private val userDetailsService: CustomUserDetailsService,
private val tokenService: TokenService,
private val jwtProperties: JwtProperties,
private val refreshTokenRepository: RefreshTokenRepository,
) {
fun authentication(authenticationRequest: AuthenticationRequest): AuthenticationResponse {
authManager.authenticate(
UsernamePasswordAuthenticationToken(
authenticationRequest.email,
authenticationRequest.password
)
)
val user = userDetailsService.loadUserByUsername(authenticationRequest.email)
val accessToken = createAccessToken(user)
val refreshToken = createRefreshToken(user)
refreshTokenRepository.save(refreshToken, user)
return AuthenticationResponse(
accessToken = accessToken,
refreshToken = refreshToken
)
}
fun refreshAccessToken(refreshToken: String): String? {
val extractedEmail = tokenService.extractEmail(refreshToken)
return extractedEmail?.let { email ->
val currentUserDetails = userDetailsService.loadUserByUsername(email)
val refreshTokenUserDetails = refreshTokenRepository.findUserDetailsByToken(refreshToken)
if (!tokenService.isExpired(refreshToken) && refreshTokenUserDetails?.username == currentUserDetails.username)
createAccessToken(currentUserDetails)
else
null
}
}
private fun createAccessToken(user: UserDetails) = tokenService.generate(
userDetails = user,
expirationDate = getAccessTokenExpiration()
)
private fun createRefreshToken(user: UserDetails) = tokenService.generate(
userDetails = user,
expirationDate = getRefreshTokenExpiration()
)
private fun getAccessTokenExpiration(): Date =
Date(System.currentTimeMillis() + jwtProperties.accessTokenExpiration)
private fun getRefreshTokenExpiration(): Date =
Date(System.currentTimeMillis() + jwtProperties.refreshTokenExpiration)
}
Resumamos qué es exactamente lo que ha cambiado aquí.
En primer lugar, importamos el RefreshTokenRepository, para poder persistir nuevos tokens y recuperar los guardados.
Además, debemos generar un nuevo token de actualización cada vez que un usuario se autentique con éxito. Y es por eso que agregamos la función getRefreshTokenExpiration y dos líneas adicionales en el método de autenticación.
En cuanto al flujo del token de actualización, se puede resumir en los siguientes pasos:
- Extraemos el correo electrónico del usuario del token de actualización pasado.
- Si este paso se completa con éxito, obtenemos los detalles actuales del usuario por el valor.
- Después de eso, buscamos los detalles del usuario persistido con el token de actualización.
- Por último, si el token de actualización no ha expirado y el correo electrónico del asunto JWT coincide con los detalles actuales del usuario, entonces se genera un nuevo token de acceso para ese usuario.
- De lo contrario, devolvemos null, lo que se traducirá en 403 Prohibido en nuestro controlador.
142.5 Apendice¶
Enlaces * Paso a paso * En medium * Ejemplo Codersee * Pagina para el ejemplo * Video explicativo * Repositorio del ejemplo codersee * Videos sobre el tema * Centrado en el login * Con openssl