리소스 서버가 2개의 인증서버의 토큰을 검증해야 하는 상황
fig1. 리소스 서버가 2개의 인증서버의 토큰을 검증해야 하는 상황

위와 같은 상황에서, 두 클라이언트의 리소스 접근 요청에 대해서 리소스 서버가 토큰의 유효성을 검증할 수 있는 방법입니다.

ResourceServerConfigurer 인터페이스에는 2개의 메시지가 있는데, void configure(ResourceServerSecurityConfigurer resources) 에서 인증(올바른 클라이언트인지)을, void configure(HttpSecurity http)에서 인가(클라이언트가 접근하려는 리소스에 대한 권한을 가지고 있는지)를 담당합니다.

public interface ResourceServerConfigurer {

  /**
   * Add resource-server specific properties (like a resource id). The defaults should work for many applications, but
   * you might want to change at least the resource id.
   * 
   * @param resources configurer for the resource server
   * @throws Exception if there is a problem
   */
  void configure(ResourceServerSecurityConfigurer resources) throws Exception;

  /**
   * Use this to configure the access rules for secure resources. By default all resources <i>not</i> in "/oauth/**"
   * are protected (but no specific rules about scopes are given, for instance). You also get an
   * {@link OAuth2WebSecurityExpressionHandler} by default.
   * 
   * @param http the current http filter configuration
   * @throws Exception if there is a problem
   */
  void configure(HttpSecurity http) throws Exception;

}

Spring boot OAuth2의 기본 설정으로는 client-id, client-secret, token-info-uri를 하나밖에 설정할 수 없기 때문에, 여러개를 받을 수 있도록 void configure(ResourceServerSecurityConfigurer resources) 를 구현해야 합니다.

# application.yml

security:
  oauth2:
    client:
      client-id: ${OAUTH_CLIENT_ID1}
      client-secret: ${OAUTH_CLIENT_SECRET1}
    resource:
      token-info-uri: ${OAUTH_TOKEN_INFO_URI1}
# 아래 값들도 spirng에서 사용할 수 있도록 ResourceServerConfigurer를 구현해야 합니다.
    client2:
      client-id: ${OAUTH_CLIENT_ID2}
      client-secret: ${OAUTH_CLIENT_SECRET2}
    resource2:
      token-info-uri: ${OAUTH_TOKEN_INFO_URI2}

스프링의 기본 ResourceServerConfigurer 구현체와 똑같은 행동을 하는 ResourceServerConfig는 다음과 같습니다.

@Configuration
public class ResourceServerConfig implements ResourceServerConfigurer {
    @Value("${security.oauth2.client.client-id}")
    private String clientId1;

    @Value("${security.oauth2.client.client-secret}")
    private String clientSecret1;

    @Value("${security.oauth2.resource.token-info-uri}")
    private String checkTokenEndpointUrl1;


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        RemoteTokenServices remoteTokenService1 = new RemoteTokenServices();
        remoteTokenService1.setClientId(clientId1);
        remoteTokenService1.setClientSecret(clientSecret1);
        remoteTokenService1.setCheckTokenEndpointUrl(checkTokenEndpointUrl1);

        resources.tokenServices(remoteTokenService1);
    }
  ...
}

매개변수인 resourcestokenServices 메시지를 보내서 remoteTokenServices1 을 등록하는데, 문제는 RemoteTokenServices클래스가 인증서버 1개의 정보만 받을 수 있다는 것입니다.

resource.tokenServices()의 매개변수 인터페이스는 ResourceServerTokenServices고, RemoteTokenServices도 이 인터페이스의 구현체입니다. 같은 인터페이스를 사용하면서 더 많은 인증정보를 받을 수 있는 구현체를 만들 수 있을까요?

public interface ResourceServerTokenServices {

  /**
   * Load the credentials for the specified access token.
   *
   * @param accessToken The access token value.
   * @return The authentication for the access token.
   * @throws AuthenticationException If the access token is expired
   * @throws InvalidTokenException if the token isn't valid
   */
  OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;

  /**
   * Retrieve the full access token details from just the value.
   * 
   * @param accessToken the token value
   * @return the full access token with client id etc.
   */
  OAuth2AccessToken readAccessToken(String accessToken);

}

생성자에서 RemoteTokenServices 를 배열로 받고, loadAuthentication이나 readAccessToken메시지를 받았을 때 위 배열을 돌면서 차례대로 메시지를 전달해주는 구현체를 만들면 됩니다.

import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.util.Assert;

/**
 * 여러 개의 ResourceServerTokenServices의 구현체를 사용하는 수 있는 ResourceServerTokenServices를 구현했습니다.
 * 생성자로 주입하는 배열의 순서대로 loadAuthentication을 호출하고, 첫 번째로 성공한 결과를 리턴합니다.
 * 모든 요청이 실패한다면 첫 번째 발생한 실패의 RuntimeException을 throws합니다.
 */
public class ResourceServerMultipleTokenServices implements ResourceServerTokenServices {
    private ResourceServerTokenServices[] resourceServerTokenServices;

    public ResourceServerMultipleTokenServices(ResourceServerTokenServices...resourceServerTokenServices) {
        Assert.notEmpty(resourceServerTokenServices, "At least single token service is required.");
        this.resourceServerTokenServices = resourceServerTokenServices;
    }

    /**
     * 인증서버에 accessToken을 보내서 OAuth2Authentication를 받아옵니다.
     * @param accessToken accessToken.
     * @return OAuth2Authentication.
     */
    public OAuth2Authentication loadAuthentication(String accessToken) {
        RuntimeException exception = null;

        for (ResourceServerTokenServices tokenService : this.resourceServerTokenServices) {
            try {
                return tokenService.loadAuthentication(accessToken);
            } catch (RuntimeException e) {
                if (exception == null) {
                    exception = e;
                }
            }
        }

        // 생성자에서 최소한 1개 이상의 ResourceServerTokenService를 주입받도록 강제하므로 exception은 null일 수 없습니다.
        throw exception;
    }

    // 아래는 RemoteTokenServices와 동일하게 구현했습니다.
    public OAuth2AccessToken readAccessToken(String accessToken) {
        throw new UnsupportedOperationException("Not supported: read access token");
    }
}

이제 ResourceServerConfig에서 여러개의 RemoteTokenServices를 생성한 뒤 RemoteTokenMultipleTokenServices에 담아 resources.tokenServices()를 호출하면 됩니다.

public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Value("${security.oauth2.client.client-id}")
    private String clientId;

    @Value("${security.oauth2.client.client-secret}")
    private String clientSecret;

    @Value("${security.oauth2.resource.token-info-uri}")
    private String checkTokenEndpointUrl;

    @Value("${security.oauth2.client2.client-id}")
    private String clientId2;

    @Value("${security.oauth2.client2.client-secret}")
    private String clientSecret2;
    
    @Value("${security.oauth2.resource2.token-info-uri}")
    private String checkTokenEndpointUrl2;


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        RemoteTokenServices remoteTokenService1 = new RemoteTokenServices();
        remoteTokenService1.setClientId(clientId);
        remoteTokenService1.setClientSecret(clientSecret);
        remoteTokenService1.setCheckTokenEndpointUrl(checkTokenEndpointUrl);

        RemoteTokenServices remoteTokenService2 = new RemoteTokenServices();
        remoteTokenService2.setClientId(clientId2);
        remoteTokenService2.setClientSecret(clientSecret2);
        remoteTokenService2.setCheckTokenEndpointUrl(checkTokenEndpointUrl2);

        resources.tokenServices(new ResourceServerMultipleTokenServices(
                remoteTokenService1,
                remoteTokenService2
        ));
    }
  ...
}

객체의 개수를 추상화함으로서 동일한 인터페이스를 구현하면서도 처리할 수 있는 매개변수의 양을 늘렸습니다.