위와 같은 상황에서, 두 클라이언트의 리소스 접근 요청에 대해서 리소스 서버가 토큰의 유효성을 검증할 수 있는 방법입니다.
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);
}
...
}
매개변수인 resources
에 tokenServices
메시지를 보내서 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
));
}
...
}
객체의 개수를 추상화함으로서 동일한 인터페이스를 구현하면서도 처리할 수 있는 매개변수의 양을 늘렸습니다.