Spring SecurityLección 11
Llamadas seguras entre servicios
Cuando un servicio llama a otro, ¿con qué identidad lo hace? Client Credentials, propagación del token del usuario, token exchange y mTLS, con RestClient y las interfaces HTTP de Spring.
Al terminar sabrás
- Elegir entre identidad de servicio, propagación del token del usuario y token exchange.
- Configurar un cliente OAuth2 con Client Credentials para un job o un microservicio.
- Añadir tokens automáticamente con
OAuth2ClientHttpRequestInterceptory@ClientRegistrationId. - Aplicar mínimo privilegio y confianza cero entre servicios.
En una arquitectura con varios servicios, las peticiones no solo vienen de usuarios: un job nocturno pide notas para un informe, un servicio de facturación consulta otro de clientes, un BFF llama a la API en nombre del usuario. Cada llamada necesita responder la misma pregunta de siempre —¿quién eres?— y la respuesta no es “estoy dentro de la red, confía en mí”.
Tres formas de identificarse
Sección titulada «Tres formas de identificarse»| Patrón | Quién aparece como sub | Cuándo |
|---|---|---|
| Client Credentials | El propio servicio (client_id) | Jobs, procesos sin usuario, llamadas de sistema |
| Propagación del token (token relay) | El usuario original | El servicio actúa en nombre del usuario y el destino acepta la misma audiencia |
| Token exchange (RFC 8693) | El usuario, con un token nuevo | Cuando el destino tiene otra audiencia o necesitas recortar permisos |
Client Credentials con RestClient
Sección titulada «Client Credentials con RestClient»Con spring-boot-starter-security-oauth2-client, el registro del cliente son propiedades:
spring: security: oauth2: client: registration: notes-api: provider: auth client-id: reports-job client-secret: ${REPORTS_CLIENT_SECRET} authorization-grant-type: client_credentials scope: notes:read provider: auth: issuer-uri: http://localhost:9000En un proceso sin petición HTTP de por medio (un job, un consumidor de mensajes), usa un AuthorizedClientServiceOAuth2AuthorizedClientManager, que no depende de la sesión del usuario:
@BeanOAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository registrations, OAuth2AuthorizedClientService clients) { var manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrations, clients); manager.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .build()); return manager;}
@BeanRestClient notesClient(RestClient.Builder builder, OAuth2AuthorizedClientManager manager) { return builder .baseUrl("http://localhost:8080") .requestInterceptor(new OAuth2ClientHttpRequestInterceptor(manager)) // añade el Bearer .build();}Y en cada llamada indicas qué registro usar:
List<NoteView> notes = notesClient.get() .uri("/api/notes") .attributes(clientRegistrationId("notes-api")) // RequestAttributeClientRegistrationIdResolver .retrieve() .body(new ParameterizedTypeReference<>() { });El interceptor pide el token la primera vez, lo reutiliza hasta que caduca y lo renueva solo.
Con interfaces HTTP (Spring Security 7)
Sección titulada «Con interfaces HTTP (Spring Security 7)»Si declaras tus clientes como interfaces HTTP de Spring Framework 7, la anotación @ClientRegistrationId sustituye al atributo:
@HttpExchange("/api")@ClientRegistrationId("notes-api")interface NotesApi {
@GetExchange("/notes") List<NoteView> list();}@BeanOAuth2RestClientHttpServiceGroupConfigurer securityConfigurer(OAuth2AuthorizedClientManager manager) { return OAuth2RestClientHttpServiceGroupConfigurer.from(manager); // aplica el interceptor a los grupos de clientes HTTP}Propagar el token del usuario
Sección titulada «Propagar el token del usuario»Si una API recibe una petición de ana y necesita llamar a otra en su nombre, puede reenviar el mismo token:
ClientHttpRequestInterceptor relay = (request, body, execution) -> { if (SecurityContextHolder.getContext().getAuthentication() instanceof JwtAuthenticationToken jwt) { request.getHeaders().setBearerAuth(jwt.getToken().getTokenValue()); } return execution.execute(request, body);};Confianza cero entre servicios
Sección titulada «Confianza cero entre servicios»- Cada servicio es un resource server: valida tokens aunque la llamada venga de “dentro”. La red interna no es una frontera de confianza.
- Un cliente por servicio, con los scopes mínimos que necesita.
reports-joblee; no escribe. - Distingue tokens de servicio y de usuario cuando importa: un token
client_credentialsno tiene un usuario detrás, así que reglas como “solo el dueño” no aplican igual. - mTLS (TLS mutuo) añade autenticación del canal: cada servicio presenta un certificado. Es habitual con un service mesh, y se combina con los tokens, no los sustituye.
Mini lab · 30 min
Un job que lee notas
Objetivo: Crear un proceso que obtenga un token con Client Credentials y lea la API de notas con permisos mínimos.
Usa el Authorization Server de la lección de OAuth2 con un cliente reports-job (client_credentials, scope notes:read).
- Crea una aplicación sin servidor web (
spring.main.web-application-type=none) con el starter de cliente OAuth2 y la configuración de arriba. - En un
CommandLineRunner, llama aGET /api/notesdos veces seguidas y registra en el log cuándo se pide el token. - Intenta un
POST /api/notescon el mismo cliente.
Preguntas:
- ¿Cuántas veces se llama a
/oauth2/tokenen el paso 2? ¿Por qué? - ¿Qué responde la API al
POSTy por qué? - Si la API filtra las notas por
owner = authentication.name, ¿qué notas ve el job? ¿Cómo darías al job acceso de lectura a todas sin darle permisos de admin humano?
Pista
Mira el sub del token de client_credentials y el scope que se le concedió.
Mostrar respuestaOcultar respuesta
- Una. El
OAuth2AuthorizedClientManagerguarda el cliente autorizado (con su token) en elOAuth2AuthorizedClientServicey lo reutiliza hasta que está a punto de caducar. - 403: el token solo tiene
SCOPE_notes:ready la regla de escritura exigeSCOPE_notes:write. Es el mínimo privilegio funcionando. - Ninguna: su
subesreports-job, que no es dueño de nada. Crea un scope específico (por ejemplonotes:report) que solo se conceda a ese cliente, y un endpoint o regla (hasAuthority("SCOPE_notes:report")) que permita leerlas todas, separado de los permisos de los administradores humanos.