CodeGym /Cursos /Spring Security /TokenService para access token

TokenService para access token

Spring Security
Nivel 23, Lección 0
Disponible

1. El problema: el token en el controlador

Cuando escribimos por primera vez un endpoint de login, la mano nos lleva instintivamente a hacerlo todo en un solo lugar: recibir el JSON, verificar usuario/contraseña, recopilar los claims, firmar el JWT y devolver la respuesta. Se siente rápido y «pragmático», especialmente si aún no has desarrollado alergia a los controladores de 300 líneas. Pero este enfoque vuelve el código de seguridad frágil y difícil de mantener.

Imaginemos un controlador típico «monolítico». Muestro intencionadamente un antiejemplo — no para que lo repitas, sino para que percibas el olor a futuros problemas:

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
class AuthController {

    @PostMapping("/login")
    Object login(@RequestBody Object request) {
        // Antiejemplo: el controlador no debe contener toda la lógica de seguridad.
        // 1) aquí debería ir "verificar contraseña" (responsabilidad de AuthenticationManager)
        // 2) aquí debería ir "recopilar claims" (responsabilidad de TokenService)
        // 3) aquí debería ir "firmar JWT"
        // 4) devolver respuesta JSON al cliente
        return "token"; // Mala señal: "cadena mágica" en lugar de un modelo de resultado
    }
}

En este punto, un principiante suele decir: «Bueno, y qué, funciona». Funciona — mientras la tarea sea pequeña. Pero luego llega la vida real: cambia la composición de los claims, se modifica el TTL, se necesita registrar el evento de inicio de sesión, se desea probar la emisión del token por separado, aparecen diferentes tipos de token, hay que manejar errores con más cuidado. Y de repente el controlador se convierte en una navaja suiza que sabe hacer de todo, excepto mantenerse legible.

En una API stateless es especialmente importante que la capa HTTP sea ligera. El controlador debe ser una puerta: «entras — sales». Pero la decisión de «qué token emitir a un usuario autenticado» es una operación de negocio separada de la capa de seguridad.

2. Roles: AuthenticationManager y TokenService

En el mundo stateless es útil pensar así: AuthenticationManager es el «control de pasaportes», y TokenService es la «oficina que emite el pase de acceso al recinto». El control de pasaportes no debe imprimir pases, y la imprenta de pases no debe decidir quién es un ciudadano real y quién llegó con una impresión hecha en Paint. La separación parece obvia en la vida real — y sin embargo, muy a menudo se olvida en el código.

Para no confundirnos, es útil fijar los roles en una tabla:

Componente Pregunta principal Qué recibe como entrada Qué entrega como salida
AuthenticationManager «¿Se puede considerar a este usuario autenticado?» credenciales (usuario/contraseña en forma de solicitud Authentication) Authentication autenticado o una excepción
TokenService «¿Qué access token emitir a un usuario ya autenticado?» Authentication exitoso (sin contraseña como objeto de negocio) modelo del token (cadena + tiempo de vida)

Y ahora, un diagrama para que la mente no solo vea palabras:

flowchart TD
  %% El cliente llama al endpoint de login
  C["Cliente"] -->|"POST /api/auth/login"| AC["AuthController"]
  %% El controlador delega la orquestación al servicio
  AC --> AS["AuthService"]
  %% Spring Security verifica las credenciales y devuelve Authentication o error
  AS --> AM["AuthenticationManager"]
  AM --> AS
  %% Tras la autenticación exitosa, se emite el access token mediante un servicio separado
  AS --> TS["TokenService"]
  TS --> AS
  AS --> AC
  AC -->|"Respuesta JSON"| C

Observa una idea importante: TokenService no debe recibir la contraseña en texto plano. En absoluto. Nada de String password «por un momento», nada de LoginRequest, nada de HttpServletRequest. No es un capricho del arquitecto, es higiene básica: cuantos menos lugares en el código «vean» la contraseña, menos posibilidades hay de que se registre accidentalmente, se almacene o se envíe a donde no debe.

Y otro detalle: TokenService no decide si la contraseña es correcta. Parte de que la Authentication ya es exitosa, por lo tanto, la contraseña ya ha sido verificada por los mecanismos estándar de Spring Security (a través de DaoAuthenticationProvider, UserDetailsService, PasswordEncoder y el estado de la cuenta).

3. Modelo de resultado: IssuedAccessToken

Cuando se emite un token, necesitamos algún lugar donde almacenar el resultado. Podríamos devolver un String directamente, pero eso lleva rápidamente a «cadenas mágicas»: en un sitio se necesita el TTL, en otro el token type, en otro se quiere añadir issuedAt, y en otro incluso habrá dos tokens. Por eso es más conveniente crear un pequeño modelo interno.

En nuestro proyecto Secure Content Platform API crearemos un objeto de resultado para la emisión del token. Para que el código sea breve y claro, podemos usar un record (en Java 25 es algo normal y cotidiano para «clases de datos»).

package com.example.securecontent.security.jwt;

/**
 * Resultado interno de la emisión de un access token.
 * Importante: no es un DTO de HTTP, sino un contrato entre la capa de seguridad y el servicio de aplicación.
 */
public record IssuedAccessToken(
        String value,         // Contenido del JWT en forma de cadena
        long expiresInSeconds // Tiempo de vida del token (TTL) en segundos
) { }

Aquí es importante entender que IssuedAccessToken es un contrato interno entre capas, no un «formato obligatorio de respuesta HTTP». ¿Por qué es fundamental?

El controlador y la capa web en general viven en el mundo de HTTP: @RequestBody, @ResponseStatus, contratos JSON y errores. Mientras que TokenService debe vivir en el mundo de la mecánica de seguridad y la decisión de negocio de «emitir un token». Si obligamos a TokenService a devolver directamente un AuthResponse (DTO para el cliente), vinculamos la capa de seguridad a la forma web, y cualquier refactorización cosmética de la API afectará a las entrañas de la seguridad.

Un modelo interno es como el empaquetado en el almacén: puede diferir del escaparate en la tienda. El almacén debe funcionar de manera eficiente y estable, mientras que el escaparate puede cambiar debido a los requisitos del cliente.

4. Interfaz TokenService

Ahora que tenemos el modelo de resultado, podemos describir el contrato del propio servicio. Aquí es importante un principio: el contrato debe ser mínimo, pero expresar el significado. No escribimos una «cosechadora universal de generación de tokens para todos los tiempos», escribimos un servicio claro para el proyecto actual y el nivel actual del curso.

La interfaz mínima tiene este aspecto:

package com.example.securecontent.security.jwt;

import org.springframework.security.core.Authentication;

public interface TokenService {

    /**
     * Emite un access token basado en una autenticación ya exitosa.
     * Importante: aquí llega Authentication después de la verificación de la contraseña.
     */
    IssuedAccessToken issueAccessToken(Authentication authentication);
}

¿Por qué la entrada es precisamente Authentication, y no UserAccount, UserDetails o String username? Porque Authentication es ya el resultado «canónico» del trabajo de Spring Security. Contiene el nombre de usuario (getName()), contiene las authorities, y ya tiene en cuenta todo lo que hemos construido antes: estado de la cuenta, roles, reglas de carga del usuario desde la base de datos.

¿Por qué es útil en la práctica, especialmente para un principiante? Si intentas pasar a TokenService algo «propio», rápidamente empezarás a hacer mapeo manual y a acordar qué se considera la verdad: en un sitio roles, en otro authorities, en otro username, en otro id, en otro accountNonLocked… Como resultado, duplicas el modelo de seguridad en tu código y aumentas la probabilidad de desincronización.

En cambio, Authentication es lo que Spring Security ya utiliza para tomar decisiones. Por lo tanto, apoyarse en él es el camino más económico.

Para sentir mejor los límites de la responsabilidad, imagina dos preguntas que le haces al código:

  • Si la pregunta es «¿es correcta la contraseña?», «¿el usuario está bloqueado o no?» — es dominio de AuthenticationManager.
  • Si la pregunta es «¿qué poner en el token? ¿qué TTL? ¿qué formato?» — es dominio de TokenService.

Todo. No mezclamos.

5. AuthService como orquestador

En nuestra aplicación habrá un endpoint POST /api/auth/login. Pero aunque aún no discutamos su contrato JSON, podemos crear desde ya el punto arquitectónico correcto: un servicio que enlace los pasos del flujo de login. Normalmente lo llamo «orquestador»: no toca todos los instrumentos a la vez, pero indica quién debe entrar en cada momento.

El esqueleto mínimo de dicho servicio:

package com.example.securecontent.auth;

import com.example.securecontent.security.jwt.TokenService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.stereotype.Service;

@Service
public class AuthService {
    // AuthenticationManager es responsable de verificar las credenciales y devolver Authentication
    private final AuthenticationManager authenticationManager;

    // TokenService es responsable de emitir el access token tras una autenticación exitosa
    private final TokenService tokenService;

    public AuthService(AuthenticationManager authenticationManager, TokenService tokenService) {
        // DI hace que las dependencias sean explícitas y comprobables
        this.authenticationManager = authenticationManager;
        this.tokenService = tokenService;
    }
}

Observa dos detalles que parecen pequeños, pero que en la práctica evitan futuros problemas.

En primer lugar, AuthService recibe AuthenticationManager, no un UserRepository cualquiera ni un PasswordEncoder. Esto nos mantiene dentro del modelo correcto: las credenciales las verifica Spring Security.

En segundo lugar, AuthService recibe TokenService, no una «clase de utilidades que genera tokens». Esto ya es arquitectura, donde la emisión del token es una responsabilidad separada.

Podrías preguntar: «¿Para qué otro servicio si podemos hacer controlador → authenticationManager → tokenService?» La respuesta es simple: porque el controlador no debe conocer los pasos internos, y porque en un proyecto real alrededor del login casi siempre hay acciones adicionales (por ejemplo, auditoría del evento de inicio de sesión, límites de intentos, normalización del login). El orquestador es el lugar donde estos pasos viven, sin inflar el controlador.

6. Paquetes para TokenService

Cuando el proyecto es pequeño, parece que «los paquetes no importan». Cuando el proyecto vive al menos un mes, de repente los paquetes se convierten en la navegación sin la cual empiezas a «buscar el servicio de tokens usando Ctrl+Mayús+F y rezando». Por lo tanto, es mejor colocar el código directamente donde se espera.

Ya hemos fijado la estructura del proyecto como package-by-feature con una zona de seguridad separada. Por lo tanto, para este día los lugares naturales son:

  • com.example.securecontent.security.jwt — para TokenService, IssuedAccessToken y futuras utilidades JWT a nivel de seguridad.
  • com.example.securecontent.auth — para AuthService y los endpoints web de la zona de autenticación (controladores y DTO), porque «auth» es la zona de aplicación del producto.

¿Por qué no poner TokenService en auth? Porque la emisión del token es parte de la infraestructura de seguridad, y será necesaria no solo «en el controlador de login», sino también como un componente separado que debe permanecer limpio y con dependencias mínimas de la capa web.

Y otra idea importante: TokenService debe ser un bean de Spring, no una public final class JwtUtils { ... } con métodos estáticos. En cuanto llegues a la configuración del secreto, TTL y otros parámetros, querrás usar DI y configuración. Además, un bean es más fácil de probar y sustituir.

Un pequeño consejo de la práctica: si ves que la lógica de seguridad está dispersa entre controller, service, util, config sin una zona clara — casi siempre significa que en un par de semanas te crearás tu propio acertijo «¿por qué el token se emite de una forma y se verifica de otra?».

7. Errores típicos al separar TokenService

Error n.º 1: generar el token directamente en el controlador.
Normalmente comienza como «voy a probar una idea rápidamente», y termina con el controlador convertido en el centro del universo: aparecen claims, TTL, firma, manejo de errores, logging y un par de TODO aleatorios. Ese código es difícil de probar, difícil de cambiar y fácil de romper accidentalmente. El hábito correcto es mantener el controlador ligero y la emisión del token en TokenService.

Error n.º 2: pasar a TokenService la contraseña en texto plano o un LoginRequest.
Si TokenService recibe la contraseña, se convierte en cómplice de la autenticación, aunque no debería serlo. Además, aumentas la superficie de fuga: la contraseña puede acabar accidentalmente en un log, en una excepción, en una traza de depuración. Es mucho más seguro y arquitectónicamente más honesto pasar a TokenService solo una Authentication exitosa, donde la contraseña ya no es un «dato de negocio» que se sigue arrastrando por el código.

Error n.º 3: hacer de TokenService una clase de utilidades estática.
Mientras generas un «token-cadena de la nada», una util estática parece cómoda. Pero en cuanto aparezcan configuraciones (secreto, TTL, issuer), empezarás a hardcodearlas en el código, o a pasar parámetros a través de una docena de métodos, o a meter @Value en la util y volverte loco. Un bean de Spring aquí es simplemente más sencillo y correcto: la configuración y las dependencias viven a través de DI, y el servicio sigue siendo comprobable.

Error n.º 4: mezclar el modelo interno del token con la respuesta HTTP.
Si TokenService devuelve un DTO de respuesta (AuthResponse), empieza a conocer el contrato de la API y la capa web. Y luego cambias el formato de la respuesta (por ejemplo, quieres devolver el campo tokenType o cambiar el nombre de accessToken), y tienes que tocar la capa de seguridad. Un objeto interno como IssuedAccessToken resuelve el problema: TokenService permanece interno, y el controlador/servicio de la capa API se encarga de empaquetarlo en el formato JSON necesario.

Error n.º 5: registrar el token completo «para depuración».
Esto suena inocente: «es que estoy en local». Pero los hábitos migran a producción más rápido de lo que parece, y los logs tienden a terminar en sistemas centralizados. Incluso en un proyecto de aprendizaje es mejor establecer la norma: no imprimir el token completo. Si es muy necesario, registra los primeros 10-15 caracteres y una marca de que el token fue emitido, sin filtrar el contenido.

1
Tarea
Spring Security, nivel 23, lección 0
Bloqueada
TokenService mínimo para Authentication preparado
TokenService mínimo para Authentication preparado
1
Tarea
Spring Security, nivel 23, lección 0
Bloqueada
Controlador delgado sobre TokenService
Controlador delgado sobre TokenService
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION