通过RestController在spring-security-oauth2-client中自定义header

kiz8lqtg  于 2023-06-21  发布在  Spring
关注(0)|答案(1)|浏览(125)

我创建了一个rest-application,它接收来自Client的请求并转到资源服务器,同时使用client_Credentials通过OAuth2进行授权。但是,授权服务器(而不是默认请求)也会等待头部中的UserId,该头部必须填充来自Client请求的数据。
我使用这个bean实现:

@Configuration
    public class WebClientConfig {
        @Bean
        OAuth2AuthorizedClientManager authorizedClientManager(
                ClientRegistrationRepository clientRegistrationRepository,
                OAuth2AuthorizedClientRepository authorizedClientRepository) {
            OAuth2AuthorizedClientProvider authorizedClientProvider =
                    OAuth2AuthorizedClientProviderBuilder.builder()
                            .clientCredentials()
                            .build();
            DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
            authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
            return authorizedClientManager;
        }
    }

我通过OAuth2 AutorizeRequest请求访问令牌:

OAuth2AutorizeRequest oAuth2AutorizeRequest = OAuth2AutorizeRequest.withClientRegistrationId("test").principal("test").build();
    OAuth2AccessToken token = authorizedClientManager.authorize(oAuth2AutorizeRequest).getAccessToken;

如何在发送POST请求时将UserId头添加到Authorization头,该请求将从Client请求中填充?
示例:

POST /oauth2/token HTTP/1.1
Host: auth-server:1111
Content-Type: application/x-www-form-urlencoded
Authorization: Basic bWVzc2FnaW5nLWNsaWVudDpzZWNyZXQ=
UserId : naW5nLbWVzcVudDpzZ
grant_type=client_credentials&scope=message.read
fdbelqdn

fdbelqdn1#

您可以尝试使用这样的@Service(前提是您在Sping Boot 属性中有一个标记为“machin”的OAuth2客户端注册):

@Service
public class ClientCredentialsService {
    
    private final WebClient tokenEndpoint;
    private final String spaceDelimitedScopes;
    
    private Map<String, AccessToken> cachedTokens = new ConcurrentHashMap<>();
    
    public ClientCredentialsService(ClientRegistrationRepository clientRegistrationRepo) {
        final var clientRegistration = clientRegistrationRepo.findByRegistrationId("machin");
        
        // According to your question, this should contain at least message.read
        this.spaceDelimitedScopes = clientRegistration.getScopes().stream().collect(Collectors.joining(" "));
        
        // In client_credentials flow, according to the spec,
        // the access token should be fetched by calling the token endpoint 
        // with client_id and client_secret passed as Basic Authorization header
        // See https://www.rfc-editor.org/rfc/rfc6749#section-4.4
        tokenEndpoint= WebClient.builder()
            .baseUrl(clientRegistration.getProviderDetails().getTokenUri())
            .filter(ExchangeFilterFunctions.basicAuthentication(clientRegistration.getClientId(), clientRegistration.getClientSecret()))
            .build();
    }

    public Mono<String> getAccessToken(String userId) {
        final var now = Instant.now();

        // cache cleanup
        this.cachedTokens = cachedTokens.entrySet().stream().filter(at -> at.getValue().expiryTime().isAfter(now)).collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue));
        
        final var cached = cachedTokens.get(userId);
        if(cached != null) {
            return Mono.just(cached.value);
        }
        
        // This is a call to an OAuth2 token endpoint,
        // using client_credentials flow and a custom header
        // See the spec linked above for the request body (content of the application/x-www-form-urlencoded body)
        return tokenEndpoint.post()
                .header("userId", userId)
                .body(BodyInserters
                        .fromFormData("grant_type", "client_credentials")
                        .with("scope", spaceDelimitedScopes))
                .retrieve()
                .bodyToMono(Map.class)
                .map(resp -> {
                    final var token = (String) resp.get("access_token");
                    final var expiry = now.plusSeconds((Long) resp.get("expires_in") - 2);
                    this.cachedTokens.put(userId, new AccessToken(token, expiry));
                    return token;
                });
    }
    
    public Mono<WebClient> getAuthorizedClient(String userId) {
        return getAccessToken(userId).map(accessToken ->  WebClient
                .builder()
                .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(accessToken))
                .build());
    }
    
    static private record AccessToken(String value, Instant expiryTime) {}
}

用法如下:

@RestController
@RequiredArgsConstructor
public class MyController {
    private static final String OAUTH2_RESOURCE_URI = "https://other.service/some-resource";

    private final ClientCredentialsService clientCredentialsService;

    @GetMapping("/something-from-an-oauth2-resource-server")
    public String getSomething(HttpServletRequest request) {
        return clientCredentialsService
                .getAuthorizedClient(request.getHeader("UserId"))
                .flatMap(client -> client
                        .get()
                        .uri(OAUTH2_RESOURCE_URI)
                        .retrieve()
                        // This should be changed with the type of DTO returned by the OAuth2 delegate
                        .bodyToMono(String.class))
                .block();
    }
}

相关问题