Spring Security 5 การแทนที่สำหรับ OAuth2RestTemplate


14

ในspring-security-oauth2:2.4.0.RELEASEชั้นเรียนเช่นOAuth2RestTemplate, OAuth2ProtectedResourceDetailsและClientCredentialsAccessTokenProviderทั้งหมดได้รับการทำเครื่องหมายว่าเลิก

จาก javadoc ในคลาสเหล่านี้ชี้ไปที่คู่มือการโยกย้ายความปลอดภัยฤดูใบไม้ผลิที่บอกว่าคนควรโยกย้ายไปยังโครงการหลักความปลอดภัยสปริง 5 อย่างไรก็ตามฉันมีปัญหาในการค้นหาว่าฉันจะใช้กรณีการใช้งานของฉันในโครงการนี้อย่างไร

เอกสารและตัวอย่างทั้งหมดพูดคุยเกี่ยวกับการรวมเข้ากับผู้ให้บริการ OAuth ส่วนที่ 3 หากคุณต้องการให้มีการตรวจสอบคำขอขาเข้าของแอปพลิเคชันของคุณและคุณต้องการใช้ผู้ให้บริการ OAuth บุคคลที่สามเพื่อยืนยันตัวตน

ในกรณีที่ใช้งานทั้งหมดที่ฉันต้องการจะทำคือการร้องขอกับRestTemplateบริการภายนอกที่ได้รับการคุ้มครองโดย OAuth ขณะนี้ฉันสร้างด้วยรหัสลูกค้าและความลับของฉันซึ่งฉันผ่านเข้าไปในOAuth2ProtectedResourceDetails OAuth2RestTemplateฉันยังมีการClientCredentialsAccessTokenProviderเพิ่มแบบกำหนดเองลงในOAuth2ResTemplateที่เพิ่งเพิ่มส่วนหัวพิเศษบางอย่างลงในคำขอโทเค็นที่ต้องการโดยผู้ให้บริการ OAuth ที่ฉันใช้

ในเอกสารประกอบ Spring-security 5 ฉันได้พบส่วนที่ระบุการปรับแต่งคำขอโทเค็นแต่อีกครั้งที่ดูเหมือนจะอยู่ในบริบทของการตรวจสอบคำขอที่เข้ามากับผู้ให้บริการ OAuth บุคคลที่สาม ยังไม่ชัดเจนว่าคุณจะใช้สิ่งนี้ร่วมกับบางสิ่งเช่น a อย่างไรClientHttpRequestInterceptorเพื่อให้แน่ใจว่าแต่ละคำขอที่ส่งออกไปยังบริการภายนอกจะได้รับโทเค็นก่อนแล้วจึงได้รับสิ่งนั้นเพิ่มเข้ากับคำขอ

นอกจากนี้ในคู่มือการโยกย้ายที่ลิงก์ด้านบนมีการอ้างอิงถึงสิ่งOAuth2AuthorizedClientServiceที่กล่าวว่ามีประโยชน์สำหรับการใช้ในเครื่องดักฟัง แต่อีกครั้งดูเหมือนว่าจะต้องอาศัยสิ่งต่าง ๆ เช่นClientRegistrationRepositoryที่ดูเหมือนจะเป็นที่ที่มันยังคงลงทะเบียนสำหรับผู้ให้บริการบุคคลที่สาม ที่จัดทำขึ้นเพื่อให้แน่ใจว่าคำขอที่เข้ามานั้นได้รับการรับรองความถูกต้อง

มีวิธีใดบ้างที่ฉันสามารถใช้ประโยชน์จากฟังก์ชั่นใหม่ใน spring-security 5 สำหรับการลงทะเบียนผู้ให้บริการ OAuth เพื่อรับโทเค็นเพื่อเพิ่มไปยังคำขอขาออกจากแอปพลิเคชันของฉัน

คำตอบ:


15

OAuth 2.0 ไคลเอ็นต์มีการรักษาความปลอดภัยของฤดูใบไม้ผลิ 5.2.x ไม่สนับสนุนแต่เพียงRestTemplate WebClientดูการอ้างอิงความปลอดภัยสปริง :

การสนับสนุนไคลเอ็นต์ HTTP

  • WebClient การรวมสำหรับสภาพแวดล้อม Servlet (สำหรับการร้องขอทรัพยากรที่มีการป้องกัน)

นอกจากนี้RestTemplateจะเลิกใช้ในรุ่นอนาคต ดูRestTemplate javadoc :

หมายเหตุ:จาก 5.0 การไม่ทำปฏิกิริยาตอบสนอง org.springframework.web.reactive.client.WebClientเป็นทางเลือกที่ทันสมัยให้RestTemplateกับการสนับสนุนที่มีประสิทธิภาพสำหรับการซิงค์และ async รวมถึงสถานการณ์การสตรีม RestTemplateจะเลิกใช้ในรุ่นอนาคตและจะไม่ได้มีคุณสมบัติใหม่ที่สำคัญเพิ่มก้าวไปข้างหน้า ดูที่WebClientส่วนของเอกสารอ้างอิง Spring Framework สำหรับรายละเอียดเพิ่มเติมและรหัสตัวอย่าง

ดังนั้นทางออกที่ดีที่สุดคือการละทิ้ง RestTemplateWebClientในความโปรดปรานของ


ใช้WebClientสำหรับการไหลของข้อมูลรับรองลูกค้า

กำหนดค่าการลงทะเบียนไคลเอนต์และผู้ให้บริการโดยทางโปรแกรมหรือใช้การกำหนดค่าอัตโนมัติของ Spring Boot

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: clientId
            client-secret: clientSecret
            authorization-grant-type: client_credentials
        provider:
          custom:
            token-uri: http://localhost:8081/oauth/token

... และOAuth2AuthorizedClientManager @Bean:

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

กำหนดค่าWebClientอินสแตนซ์ที่จะใช้ServerOAuth2AuthorizedClientExchangeFilterFunctionกับที่มีให้OAuth2AuthorizedClientManager:

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("custom");
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}

ตอนนี้ถ้าคุณพยายามที่จะทำการร้องขอโดยใช้WebClientอินสแตนซ์นี้มันจะขอโทเค็นจากเซิร์ฟเวอร์การอนุญาตและรวมไว้ในการร้องขอ


ขอบคุณที่ล้างบางสิ่งออกไป แต่จากเอกสารที่เชื่อมโยงทั้งหมดข้างต้นฉันยังคงดิ้นรนเพื่อหาตัวอย่างที่ตัวดักจับ (หรือสิ่งที่เป็นศัพท์ใหม่สำหรับWebClient) หรือสิ่งที่คล้ายกันใช้เพื่อดึงโทเค็น OAuth จาก ผู้ให้บริการ OAuth ที่กำหนดเอง (ไม่ใช่หนึ่งในนั้นที่รองรับ OoTB เช่น Facebook / Google) เพื่อเพิ่มลงในคำขอขาออก ตัวอย่างทั้งหมดดูเหมือนจะมุ่งเน้นไปที่การตรวจสอบคำขอขาเข้ากับผู้ให้บริการรายอื่น คุณมีพอยน์เตอร์สำหรับตัวอย่างที่ดีบ้างไหม?
Matt Williams

1
@MattWilliams ฉันได้อัปเดตคำตอบพร้อมตัวอย่างวิธีการใช้WebClientกับประเภทการให้ข้อมูลรับรองลูกค้า
Anar Sultanov

เพอร์เฟคทุกอย่างสมเหตุสมผลแล้วขอบคุณมาก ฉันอาจจะไม่ได้รับโอกาสลองดูสักสองสามวัน แต่จะต้องแน่ใจว่าได้กลับมาและทำเครื่องหมายว่าเป็นคำตอบที่ถูกต้องเมื่อฉันไป
Matt Williams

1
ที่ตอนนี้เลิกเกินไปครับ ... อย่างน้อย UnAuthenticatedServerOAuth2AuthorizedClientRepository มี ...
ค้อนขนาดใหญ่

ขอบคุณ @SledgeHammer ฉันได้อัปเดตคำตอบแล้ว
Anar Sultanov

1

คำตอบข้างต้นจาก @Anar Sultanov ช่วยให้ฉันมาถึงจุดนี้ แต่เนื่องจากฉันต้องเพิ่มส่วนหัวเพิ่มเติมลงในคำขอโทเค็น OAuth ของฉันฉันคิดว่าฉันจะให้คำตอบแบบเต็มสำหรับวิธีการที่ฉันแก้ไขปัญหาสำหรับกรณีการใช้งานของฉัน

กำหนดค่ารายละเอียดผู้ให้บริการ

เพิ่มรายการต่อไปนี้เพื่อ application.properties

spring.security.oauth2.client.registration.uaa.client-id=${CLIENT_ID:}
spring.security.oauth2.client.registration.uaa.client-secret=${CLIENT_SECRET:}
spring.security.oauth2.client.registration.uaa.scope=${SCOPE:}
spring.security.oauth2.client.registration.uaa.authorization-grant-type=client_credentials
spring.security.oauth2.client.provider.uaa.token-uri=${UAA_URL:}

ใช้งานแบบกำหนดเอง ReactiveOAuth2AccessTokenResponseClient

ServerOAuth2AuthorizedClientExchangeFilterFunctionเช่นนี้คือการสื่อสารเซิร์ฟเวอร์ไปยังเซิร์ฟเวอร์ที่เราจำเป็นต้องใช้ นี้จะยอมรับไม่ได้ที่ไม่ได้มีปฏิกิริยาReactiveOAuth2AuthorizedClientManager OAuth2AuthorizedClientManagerดังนั้นเมื่อเราใช้ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(เพื่อให้ผู้ให้บริการเพื่อใช้ในการทำการร้องขอ OAuth2) เรามีที่จะให้มันแทนไม่ใช่ปฏิกิริยาReactiveOAuth2AuthorizedClientProvider OAuth2AuthorizedClientProviderตามเอกสารอ้างอิงการรักษาความปลอดภัยสปริงถ้าคุณใช้แบบไม่ตอบสนองDefaultClientCredentialsTokenResponseClientคุณสามารถใช้.setRequestEntityConverter()วิธีการเปลี่ยนคำขอโทเค็น OAuth2 แต่การเทียบเท่าปฏิกิริยาWebClientReactiveClientCredentialsTokenResponseClientไม่ได้ให้ความสะดวกนี้เราจึงต้องดำเนินการเอง (เราสามารถใช้ประโยชน์จากWebClientReactiveClientCredentialsTokenResponseClientตรรกะที่มีอยู่)

การใช้งานของฉันถูกเรียกUaaWebClientReactiveClientCredentialsTokenResponseClient(การดำเนินการถูกละไว้เพราะเพียงเล็กน้อยเท่านั้นที่เปลี่ยนแปลงheaders()และbody()วิธีการจากค่าเริ่มต้นWebClientReactiveClientCredentialsTokenResponseClientเพื่อเพิ่มเขตข้อมูลส่วนหัว / ร่างกายพิเศษบางอย่างมันไม่เปลี่ยนกระแสการรับรองความถูกต้องพื้นฐาน)

กำหนดค่า WebClient

ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()วิธีการได้รับการคัดค้านจึงทำตามคำแนะนำการเลิกจากวิธีการที่:

เลิก ใช้ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)แทน สร้างตัวอย่างของClientCredentialsReactiveOAuth2AuthorizedClientProviderการกำหนดค่าด้วยWebClientReactiveClientCredentialsTokenResponseClient(หรือหนึ่งเอง) DefaultReactiveOAuth2AuthorizedClientManagerและกว่าอุปทานมัน

สิ่งนี้จบลงที่การกำหนดค่าที่มีลักษณะดังนี้:

@Bean("oAuth2WebClient")
public WebClient oauthFilteredWebClient(final ReactiveClientRegistrationRepository 
    clientRegistrationRepository)
{
    final ClientCredentialsReactiveOAuth2AuthorizedClientProvider
        clientCredentialsReactiveOAuth2AuthorizedClientProvider =
            new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
    clientCredentialsReactiveOAuth2AuthorizedClientProvider.setAccessTokenResponseClient(
        new UaaWebClientReactiveClientCredentialsTokenResponseClient());

    final DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager =
        new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository,
            new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(
        clientCredentialsReactiveOAuth2AuthorizedClientProvider);

    final ServerOAuth2AuthorizedClientExchangeFilterFunction oAuthFilter =
        new ServerOAuth2AuthorizedClientExchangeFilterFunction(defaultReactiveOAuth2AuthorizedClientManager);
    oAuthFilter.setDefaultClientRegistrationId("uaa");

    return WebClient.builder()
        .filter(oAuthFilter)
        .build();
}

ใช้WebClientตามปกติ

oAuth2WebClientถั่วคือตอนนี้พร้อมที่จะใช้ในการเข้าถึงทรัพยากรการป้องกันโดยการให้บริการของเรา OAuth2 กำหนดค่าในแบบที่คุณจะทำให้คำขออื่น ๆ WebClientใช้


ฉันจะส่งผ่านรหัสลูกค้า, ไคลเอนต์ลับและจุดสิ้นสุด oauth โดยทางโปรแกรมได้อย่างไร
monti

ฉันไม่ได้ลองสิ่งนี้ แต่ดูเหมือนว่าคุณสามารถสร้างอินสแตนซ์ของ ClientRegistrations พร้อมรายละเอียดที่ต้องการและส่งต่อไปยังตัวสร้างสำหรับInMemoryReactiveClientRegistrationRepository(การเริ่มต้นใช้งานของReactiveClientRegistrationRepository) จากนั้นคุณใช้InMemoryReactiveClientRegistrationRepositoryถั่วที่สร้างขึ้นใหม่แทน autowired ของฉันclientRegistrationRepositoryที่ถูกส่งผ่านไปยังoauthFilteredWebClientวิธีการนั้น
Matt Williams

อืม แต่ฉันไม่สามารถลงทะเบียนที่แตกต่างกันClientRegistrationตอนรันไทม์ได้ไหม เท่าที่ฉันเข้าใจฉันต้องสร้างถั่วClientRegistrationเมื่อเริ่มต้น
monti

อ่าฉันคิดว่าคุณแค่ไม่ต้องการประกาศมันในapplication.propertiesไฟล์ การใช้งานของคุณเองReactiveOAuth2AccessTokenResponseClientทำให้คุณสามารถร้องขอสิ่งใดก็ได้ที่คุณต้องการรับโทเค็น OAuth2 แต่ฉันไม่ทราบว่าคุณสามารถให้บริบท "แบบไดนามิก" ต่อคำขอได้อย่างไรสิ่งเดียวกันจะเกิดขึ้นหากคุณใช้ตัวกรองทั้งหมดของคุณเองทั้งหมดนี้ จะให้คุณเข้าถึงเป็นคำขอขาออกดังนั้นหากคุณไม่สามารถอนุมานสิ่งที่คุณต้องการจากที่นั่นฉันไม่แน่ใจว่าตัวเลือกของคุณคืออะไรกรณีการใช้งานของคุณคืออะไรทำไมคุณไม่ทราบการลงทะเบียนที่เป็นไปได้เมื่อเริ่มต้น?
Matt Williams

1

ฉันพบว่า @matt Williams ตอบค่อนข้างเป็นประโยชน์ แม้ว่าฉันต้องการเพิ่มในกรณีที่มีคนต้องการเขียนรหัสลูกค้าและเป็นความลับสำหรับการกำหนดค่า WebClient นี่เป็นวิธีที่สามารถทำได้

 @Configuration
    public class WebClientConfig {

    public static final String TEST_REGISTRATION_ID = "test-client";

    @Bean
    public ReactiveClientRegistrationRepository clientRegistrationRepository() {
        var clientRegistration = ClientRegistration.withRegistrationId(TEST_REGISTRATION_ID)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .clientId("<client_id>")
                .clientSecret("<client_secret>")
                .tokenUri("<token_uri>")
                .build();
        return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
    }

    @Bean
    public WebClient testWebClient(ReactiveClientRegistrationRepository clientRegistrationRepo) {

        var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepo,  new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
        oauth.setDefaultClientRegistrationId(TEST_REGISTRATION_ID);

        return WebClient.builder()
                .baseUrl("https://.test.com")
                .filter(oauth)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    }
}

0

สวัสดีบางทีมันอาจจะสายเกินไปอย่างไรก็ตาม RestTemplate ยังคงรองรับใน Spring Security 5 สำหรับแอปที่ไม่ตอบสนอง RestTemplate ยังคงใช้สิ่งที่คุณต้องทำคือกำหนดค่าความปลอดภัยสปริงอย่างเหมาะสมเท่านั้นและสร้างตัวดักตามที่กล่าวไว้ในคู่มือการโยกย้าย

ใช้การกำหนดค่าต่อไปนี้เพื่อใช้โฟลว์ client_credentials

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${okta.oauth2.issuer}/v1/keys
      client:
        registration:
          okta:
            client-id: ${okta.oauth2.clientId}
            client-secret: ${okta.oauth2.clientSecret}
            scope: "custom-scope"
            authorization-grant-type: client_credentials
            provider: okta
        provider:
          okta:
            authorization-uri: ${okta.oauth2.issuer}/v1/authorize
            token-uri: ${okta.oauth2.issuer}/v1/token

กำหนดค่าเป็น OauthResTemplate

@Configuration
@RequiredArgsConstructor
public class OAuthRestTemplateConfig {

    public static final String OAUTH_WEBCLIENT = "OAUTH_WEBCLIENT";

    private final RestTemplateBuilder restTemplateBuilder;
    private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
    private final ClientRegistrationRepository clientRegistrationRepository;

    @Bean(OAUTH_WEBCLIENT)
    RestTemplate oAuthRestTemplate() {
        var clientRegistration = clientRegistrationRepository.findByRegistrationId(Constants.OKTA_AUTH_SERVER_ID);

        return restTemplateBuilder
                .additionalInterceptors(new OAuthClientCredentialsRestTemplateInterceptorConfig(authorizedClientManager(), clientRegistration))
                .setReadTimeout(Duration.ofSeconds(5))
                .setConnectTimeout(Duration.ofSeconds(1))
                .build();
    }

    @Bean
    OAuth2AuthorizedClientManager authorizedClientManager() {
        var authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials()
                .build();

        var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

}

Interceptor

public class OAuthClientCredentialsRestTemplateInterceptor implements ClientHttpRequestInterceptor {

    private final OAuth2AuthorizedClientManager manager;
    private final Authentication principal;
    private final ClientRegistration clientRegistration;

    public OAuthClientCredentialsRestTemplateInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
        this.manager = manager;
        this.clientRegistration = clientRegistration;
        this.principal = createPrincipal();
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
                .withClientRegistrationId(clientRegistration.getRegistrationId())
                .principal(principal)
                .build();
        OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
        if (isNull(client)) {
            throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
        }

        request.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + client.getAccessToken().getTokenValue());
        return execution.execute(request, body);
    }

    private Authentication createPrincipal() {
        return new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return Collections.emptySet();
            }

            @Override
            public Object getCredentials() {
                return null;
            }

            @Override
            public Object getDetails() {
                return null;
            }

            @Override
            public Object getPrincipal() {
                return this;
            }

            @Override
            public boolean isAuthenticated() {
                return false;
            }

            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            }

            @Override
            public String getName() {
                return clientRegistration.getClientId();
            }
        };
    }
}

สิ่งนี้จะสร้าง access_token ในการโทรครั้งแรกและเมื่อใดก็ตามที่โทเค็นหมดอายุ OAuth2AuthorizedClientManager จะจัดการทั้งหมดนี้ให้คุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.