Securizar una aplicación utilizando el estándar OAuth 2.0 mediante Spring Authorization Server y Spring Resource Server

Índice de contenidos

  1. Introducción
  2. Spring Authorization Server
    2.1 Dependencias
    2.2 Configuración
    2.3 Probando nuestro Authorization Server
  3. Spring Resource Server
    3.1 Dependencias
    3.2 Configuración
    3.3 Probando nuestro Resource Server
  4. Conclusiones

Introducción

En el mundo digital actual, la seguridad de las aplicaciones es una preocupación fundamental. A medida que estas se vuelven más sofisticadas y se conectan con diversos usuarios y servicios, es esencial garantizar que solo las personas autorizadas tengan acceso a la información y los recursos adecuados. Aquí es donde entran en juego las dos piezas sobre las que trata este tutorial, los servidores de autorización (Spring Authorization Server) y recursos (Spring Resource Server).

Spring Authorization Server nos permite implementar un servidor de autorización, emitiendo y validando tokens de acceso para autenticar y autorizar las solicitudes de los usuarios. Al utilizar Spring Authorization Server, podemos establecer políticas de seguridad flexibles y personalizadas para nuestras aplicaciones, gestionando de manera eficiente los permisos y los roles de los usuarios.

Por otro lado, Spring Resource Server se encarga de proteger los recursos de nuestra aplicación y asegurarse de que solo los usuarios autorizados puedan acceder a ellos. Funciona en conjunto con Spring Authorization Server para validar los tokens de acceso y garantizar que el usuario que solicita los recursos tenga los permisos necesarios. Al integrar Spring Resource Server en nuestra aplicación, podemos asegurar endpoints específicos y proteger la confidencialidad e integridad de los datos sensibles.

Spring Authorization Server

A continuación, realizaremos los pasos necesarios para poner en funcionamiento nuestro servidor de autorización.

Dependencias

Al ser Spring Authorization Server una capa construida por encima de Spring Security, necesitamos añadir también esta dependencia, al igual que Spring Web para el manejo de solicitudes y respuestas HTTP que utiliza Spring Authorization Server para realizar las tareas de autenticación y autorización.

Podemos hacer uso de Spring Initializr para facilitar la creación del proyecto.


Text
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Configuración

En este ejemplo, vamos a realizar una configuración básica de Spring Authorization Server. Este framework ofrece muchas posibilidades de configuración y dependerá mucho de las necesidades del proyecto en el que estemos implementándolo.

En primer lugar, vamos a configurar el puerto en el que se ejecutará el servidor de autorización en nuestro .properties. Es necesario ya que no configuraremos ninguno en el servidor de recursos y utilizará por defecto el 8080:


Text
 server.port=9000

A continuación, mediante una clase de

Text
@Configuration

de Spring, vamos a añadir los componentes necesarios:


Text
@Configuration
@EnableWebSecurity
public class AuthorizationServerSecurityConfig {

    @Bean
        @Order(1)
        public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
                        throws Exception {
                OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());
        http.exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor(
                new LoginUrlAuthenticationEntryPoint("/login"),
                new MediaTypeRequestMatcher(MediaType.TEXT_HTML))
        ).oauth2ResourceServer(resourceServer -> resourceServer
                .jwt(Customizer.withDefaults())
        );

        return http.build();
        }

        @Bean
        @Order(2)
        public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
                        throws Exception {
                http
                        .authorizeHttpRequests((authorize) -> authorize
                                .anyRequest().authenticated()
                        )
                        .formLogin(Customizer.withDefaults());

                return http.build();
        }
  • Text
    authorizationServerSecurityFilterChain

    : se aplican las configuraciones de seguridad predeterminadas y se habilita OpenID Connect 1.0.

  • Text
    defaultSecurityFilterChain

    : se configura la autorización para que todas las solicitudes requieran autenticación y se define una forma de inicio de sesión predeterminada.


Text
    @Bean
    public UserDetailsService userDetailsService() {
            UserDetails userDetails = User.withDefaultPasswordEncoder()
                            .username("admin")
                            .password("password")
                            .roles("ADMIN")
                            .build();

            return new InMemoryUserDetailsManager(userDetails);
    }
  • Text
    userDetailsService

    : crea un objeto

    Text
    UserDetailsManager

    en memoria que contiene los detalles de un usuario de prueba.


Text
    @Bean
        public RegisteredClientRepository registeredClientRepository() {
                RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                                .clientId("oidc-client")
                                .clientSecret("{noop}secret")
                                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                                .redirectUri("https://oauthdebugger.com/debug")
                                .scope(OidcScopes.OPENID)
                                .build();

                return new InMemoryRegisteredClientRepository(oidcClient);
        }
  • Text
    registeredClientRepository

    : crea un objeto

    Text
    RegisteredClientRepository

    en memoria que define un cliente registrado para el servidor de autorización. Los parámetros que se están configurando son los siguientes:

    • Text
      .clientId

      identifica al cliente que solicita acceso a los recursos protegidos, mientras que

      Text
      .clientSecret

      es una clave secreta utilizada para autenticar al cliente ante el servidor de autorización. Ambos valores son necesarios para validar y autorizar las solicitudes del cliente en el servidor.

    • Text
      clientAuthenticationMethod

      : utilizamos

      Text
       ClientAuthenticationMethod.CLIENT_SECRET_BASIC

      , el cliente envía su

      Text
      ClientId

      y

      Text
      ClientSecret

      en el encabezado de la solicitud utilizando la autenticación básica (Basic Authentication). Se envía el

      Text
      ClientId

      y

      Text
      ClientSecret

      concatenados en un mismo string y codificados en base64.

    • Text
      authorizationGrantTypes

      :

      • Text
        AuthorizationGrantType.AUTHORIZATION_CODE

        : Se utiliza para obtener un token de acceso en nombre del usuario final mediante un authorization code obtenido del servidor de autorización. Es útil para acceder a recursos protegidos en nombre del usuario.

      • Text
        AuthorizationGrantType.REFRESH_TOKEN

        : Se utiliza para obtener un nuevo token de acceso cuando el token actual ha expirado. Permite renovar el token sin requerir que el usuario vuelva a autenticarse. Es útil para mantener la sesión activa y prolongar el acceso a los recursos.

    • Text
      redirectUri("https://oauthdebugger.com/debug")

      especifica la URL a la cual el servidor de autorización redirige al cliente junto al authorization code generado una vez el usuario haya dado su consentimiento. También comprobará en el momento en el que se haga la petición para generar el authorization code que esta

      Text
      redirectUri

      está registrada en el servidor. En este caso utilizamos esa ya que vamos a apoyarnos en oauthdebugger para probar el flujo.


Text
    @Bean
        public JWKSource jwkSource() {
                KeyPair keyPair = generateRsaKey();
                RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
                RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
                RSAKey rsaKey = new RSAKey.Builder(publicKey)
                                .privateKey(privateKey)
                                .keyID(UUID.randomUUID().toString())
                                .build();
                JWKSet jwkSet = new JWKSet(rsaKey);
                return new ImmutableJWKSet<>(jwkSet);
        }

        private static KeyPair generateRsaKey() {
                KeyPair keyPair;
                try {
                        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
                        keyPairGenerator.initialize(2048);
                        keyPair = keyPairGenerator.generateKeyPair();
                }
                catch (Exception ex) {
                        throw new IllegalStateException(ex);
                }
                return keyPair;
        }

        @Bean
        public JwtDecoder jwtDecoder(JWKSource jwkSource) {
                return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
        }
  • Text
    jwkSource

    : genera un par de claves RSA y crea un objeto

    Text
    JWKSource

    que contiene la clave pública y privada en un conjunto de claves JSON. Esto se utiliza para la descodificación y verificación de los tokens JWT.

  • Text
    jwtDecoder

    : configura un decodificador JWT utilizando la fuente de claves JWK generada anteriormente.


Text
    @Bean
        public AuthorizationServerSettings authorizationServerSettings() {
                return AuthorizationServerSettings.builder().build();
        }

}
  • Text
     authorizationServerSettings

    : crea un objeto

    Text
    AuthorizationServerSettings

    que encapsula las configuraciones del servidor de autorización.

 
De esta manera ya tendríamos todo lo necesario para hacer funcionar nuestro Spring Authorization Server.

Sin embargo, tenemos más opciones de configuración que podemos realizar, una de ellas es la personalización del JWT.

En uno de los pasos previos, hemos dado de alta en memoria un usuario con el rol «ADMIN», podemos añadir esta información a nuestro JWT de la siguiente manera:


Text
@Configuration
public class JwtTokenCustomizerConfig {

@Bean
public OAuth2TokenCustomizer tokenCustomizer() {
    return (context) -> {
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            Authentication principal = context.getPrincipal();
            Set authorities = principal.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toSet());
            context.getClaims().claim("roles", authorities);
                        }
                };
        }

}

Probando nuestro Authorization Server

Una vez ejecutemos nuestro servidor de autorización, podemos conocer todos los endpoint haciendo una petición a

Text
http://localhost:9000/.well-known/openid-configuration

que nos devolverá lo siguiente:

authorizacion server configuration

En este caso, nos interesa el endpoint de autorización(

Text
/oauth2/authorize

) que utilizaremos en oauthdebugger para iniciar el proceso de autorización como se muestra a continuación:

aouthdebugger authorize request

Como se puede apreciar, a parte de los campos que hemos configurado previamente, aparecen dos nuevos:

  • Text
    Nonce

    : es un valor único generado por el cliente antes de realizar una solicitud de autorización. Se utiliza para prevenir ataques de reproducción y garantizar la integridad de los tokens de acceso y los tokens de identificación. El servidor de autorización incluirá este valor en la respuesta junto con el token de acceso, y el cliente debe verificar que el

    Text
    nonce

    devuelto coincida con el valor originalmente enviado.

  • Text
    State

    : es un parámetro opcional utilizado para mantener el estado del cliente entre la solicitud de autorización y la respuesta del servidor de autorización. El cliente envía un valor aleatorio en el parámetro

    Text
    state

    durante la solicitud de autorización, y el servidor de autorización lo incluirá en la respuesta de redireccionamiento. Esto permite que el cliente recupere el estado original y realice las acciones correspondientes, como redirigir al usuario a la página correcta después de la autorización.

 
Al realizar la petición nos enviará a la página /login para que introduzcamos las credenciales del usuario:

spring security login page

Una vez hagamos login nos redirigirá de nuevo a oauthdebugger el cual nos devolverá el Authorization code que utilizaremos para recuperar los tokens.

oauthdebugger authorization code flow success

Podemos ayudarnos de una herramienta como postman para realizar la petición a

Text
/oauth2/token

que nos devolverá los tokens:

  • Como hemos establecido en nuestra configuración, indicarle a la herramienta que vamos a hacer uso de Basic Authorization e introducir las credenciales:basic authorization
  • Realizar la petición con los datos necesarios:

De esta manera habríamos completado nuestro Authorization Code Flow.
 
Por último, vamos a hacer uso de la herramienta jwt para comprobar que dentro del access_token, se encuentra el rol que introdujimos mediante configuración.authorization response access-token information

Spring Resource Server

Al igual que hemos hecho con Spring Authorization Server, vamos a configurar lo necesario para poner en funcionamiento nuestro Resource Server.

Dependencias


Text
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Configuración

En primer lugar, en nuestro archivo

Text
.properties

indicamos nuestro issuer de los tokens JWT(Authoriation Server) que permitirá que a la hora de acceder a los recursos, se compruebe la autenticidad de los mimos.


Text
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000

Seguidamente, una clase de

Text
@Configuration

para establecer los componentes necesarios:


Text
@Configuration
@EnableMethodSecurity
public class ResourceServerConfig {

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                .oauth2ResourceServer(oAuth2 -> oAuth2
                        .jwt(it -> it.decoder(JwtDecoders.fromIssuerLocation(issuer))))
                .build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }

}
  • Añadimos
    Text
    @EnableMethodSecurity

    para habilitar la seguridad y tener control sobre el acceso y los permisos de forma más detallada.

  • Text
    securityFilterChain

    define un filtro de seguridad que garantice que todas las peticiones están autenticadas y que utilicen tokens JWT para la autenticación, extrayendo las authorities del token y estableciéndolas en el contexto de autenticación.

  • Text
    jwtAuthenticationConverter

    en el que se configura un conversor de Authorities, en este caso los roles, para establecerlos en el contexto de autenticación.

 
Por último, vamos a crear un controlador y aplicar la capa de seguridad para permitir los accesos según los roles del usuario autenticado:


Text
@RestController
@RequestMapping("/resources")
public class ResourceController {

    @GetMapping("/user")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity user(Authentication authentication) {
        return ResponseEntity.ok(authentication.getName() + " access");
    }

    @GetMapping("/admin")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public ResponseEntity admin(Authentication authentication) {
        return ResponseEntity.ok(authentication.getName() + " access");
    }
}

Con la notación

Text
@PreAuthorize("hasAuthority('roleName')")

estamos indicando que el método solo puede ejecutarse si el usuario autenticado tiene el authority (rol) especificado.

Probando nuestro Resource Server

Podemos comprobar su funcionamiento realizando el mismo flujo de autorización previo y utilizar el access_token que nos devuelve la petición con el rol para hacer las llamadas a los endpoints securizados:

spring resource call admin endpoint

Como nuestro usuario tiene el rol «ADMIN», la petición funciona correctamente.

spring resource call user endpoint

En el caso llamar al endpoint de user, nos devolverá de manera acertada un

Text
403 Forbidden

ya que no poseemos este rol.

Conclusiones

En este tutorial, hemos aprendido mediante Spring Authorization Server y Spring Resource Server a implementar fácilmente flujos de autenticación y autorización en una aplicación.
El código utilizado está disponible en el siguiente repositorio.

Comentarios

5 respuestas

  1. Hola, esta bien pero como se puede acceder desde angular, tambien si se puede poner el servidor de recursos y de autorizacion en el mismo projecto y no dos proyectos separados con puertos distintos

  2. Muy bien explicado, gracias por subir el repo, me di cabezazos durante un rato y luego de ver tu repo me di cuenta que el problema lo estaba haciendo mi jdk22

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

He leído y acepto la política de privacidad

Información básica acerca de la protección de datos

  • Responsable: IZERTIS S.A.
  • Finalidad: Envío información de carácter administrativa, técnica, organizativa y/o comercial sobre los productos y servicios sobre los que se nos consulta.
  • Legitimación: Consentimiento del interesado
  • Destinatarios: Otras empresas del Grupo IZERTIS. Encargados del tratamiento.
  • Derechos: Acceso, rectificación, supresión, cancelación, limitación y portabilidad de los datos.
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad

Consultor tecnológico de desarrollo de proyectos informáticos. Puedes encontrarme en Autentia: Ofrecemos servicios de soporte a desarrollo, factoría y formación. Somos expertos en Java/Java EE

¿Quieres publicar en Adictos al trabajo?

Te puede interesar

22/06/2026

Juan Antonio Jiménez Torres

Vamos a instrumentalizar un SDD para un microservicio bancario. En este artículo sobre SDD - Spec Driven Development, instrumentalizado sobre OpenSpec vamos a construir una PoC de algo más serio que una tabla periódica. Un microservicio bancario muy simplificado que gestione cuentas y permita hacer transferencias

16/06/2026

Juan Antonio Jiménez Torres

Este es el segundo artículo sobre SDD, Spec Driven Development, instrumentalizado sobre OpenSpec. Pero esta vez nos acercaremos usando softwere gratuito. Por un lado OpenCode en lugar de ClaudeCode y usando el modelo gratuito Gemma4 de Google en lugar de otros de pago.

08/06/2026

Juan Antonio Jiménez Torres

Aprenderemos sobre SDD – Spec Driven Development instrumentalizado sobre OpenSpec. Haremos un primer acercamiento usando ClaudeCode con Sonnet, para luego, en un artículo posterior pasarnos a la versión gratuita de OpenCode con Gemma4