带有 OpenAPI/Swagger-UI 的 Spring 资源服务器 (OAuth2.1)

问题描述 投票:0回答:1

背景:我有 2 个应用程序:

  1. Spring 授权服务器通过授权码流程对用户进行身份验证。另一种情况也与 PKCE 相关。
    http://192.168.0.4:8001
  2. Spring 资源服务器,对接受来自 Spring 授权服务器的令牌的帐户执行 HTTP.GET、HTTP.POST、HTTP.PATCH、HTTP.DELETE。资源服务器是具有 OpenAPI/Swagger 依赖项的资源服务器。
    http://192.168.0.4:8002

我注意到,在重定向过程中,重定向 javascript 中的 HTTP.POST

/oauth2/token
方法不会附加某些属性。即 Cookie 和一些标头(基本身份验证代码/密钥)。这可能是问题的根源。 Spring OAuth 服务器需要这些来维护状态。

资源服务器:

构建.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.3'
    id 'io.spring.dependency-management' version '1.1.3'
    id 'org.hibernate.orm' version '6.2.7.Final'
    id 'org.graalvm.buildtools.native' version '0.9.24'
}

group = 'io.resource'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'io.micrometer:micrometer-tracing-bridge-brave'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testImplementation 'org.springframework.boot:spring-boot-testcontainers'
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:postgresql'
}

tasks.named('test') {
    useJUnitPlatform()
}

hibernate {
    enhancement {
        enableAssociationManagement = true
    }
}


重定向脚本:

<!doctype html>
<html lang="en-US">
<head>
    <title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
    'use strict';

    function parseQueryParams(query) {
        let params = {};
        query.split('&').forEach(function(part) {
            let item = part.split('=');
            params[item[0]] = decodeURIComponent(item[1]);
        });
        return params;
    }

    function run() {
        let oauth2 = window.opener.swaggerUIRedirectOauth2;
        let sentState = oauth2.state;
        let redirectUrl = oauth2.redirectUrl;
        let qp = window.location.hash.substring(1) || location.search.substring(1);

        let params = parseQueryParams(qp);
        let isValid = params.state === sentState;

        let errorCb = function(message) {
            oauth2.errCb({
                authId: oauth2.auth.name,
                source: "auth",
                level: "error",
                message: message
            });
        };

        if (['accessCode', 'authorizationCode', 'authorization_code'].includes(oauth2.auth.schema.get("flow")) && !oauth2.auth.code) {
            if (!isValid) {
                errorCb("Authorization may be unsafe, passed state was changed in server. Passed state wasn't returned from auth server.");
                return;
            }
            if (params.code) {
                delete oauth2.state;
                oauth2.auth.code = params.code;
                oauth2.callback({ auth: oauth2.auth, redirectUrl: redirectUrl });
            } else {
                errorCb(params.error ? `[${params.error}]: ${params.error_description || 'no accessCode received from the server'}. ${params.error_uri || ''}` : "[Authorization failed]: no accessCode received from the server");
            }
        } else {
            oauth2.callback({ auth: oauth2.auth, token: params, isValid: isValid, redirectUrl: redirectUrl });
        }
        window.close();
    }

    window.addEventListener('DOMContentLoaded', run);
</script>
</body>
</html>

OpenAPI/Swagger-UI 配置:

package io.resource.account.spring.config.swagger;

import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.OAuthFlow;
import io.swagger.v3.oas.annotations.security.OAuthFlows;
import io.swagger.v3.oas.annotations.security.OAuthScope;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;
import java.util.Collections;

@Configuration
@SecurityScheme(
        name = "security_auth",
        type = SecuritySchemeType.OAUTH2,
        flows = @OAuthFlows(
                authorizationCode = @OAuthFlow(
                        authorizationUrl = "${springdoc.oAuthFlow.authorization-url}",
                        tokenUrl = "${springdoc.oAuthFlow.token-url}",
                        scopes = {
                                @OAuthScope(name = "account.create", description = "Create account"),
                                @OAuthScope(name = "account.read", description = "Read account"),
                                @OAuthScope(name = "account.update", description = "Update account"),
                                @OAuthScope(name = "account.delete", description = "Delete account")
                        })))
public class OpenAPIConfig {

    @Bean
    public OpenAPI gateWayOpenApi() {
        return new OpenAPI().info(new Info().title("Accounts Service")
                        .description("Accounts Microservice Swagger API")
                        .version("v1.0.0")
                        .contact(new Contact()
                                .name("Accounts Dev Team")
                                .email("[email protected]")))
                .addSecurityItem(new SecurityRequirement()
                        .addList("bearer-jwt", Arrays.asList("account.create", "account.read", "account.update", "account.delete"))
                        .addList("bearer-key", Collections.emptyList()));
    }
}

资源服务器配置:

package io.resource.account.spring.config.security;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;

@Log4j2
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ResourceServerConfig {

    // @formatter:off
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll())
                .authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.GET, "/auth").fullyAuthenticated())
                .authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.POST, "/accounts").hasAuthority("SCOPE_account.create"))
                .authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.GET, "/accounts/**", "/accounts").hasAuthority("SCOPE_account.read"))
                .authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.PATCH, "/accounts/**").hasAuthority("SCOPE_account.update"))
                .authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.DELETE, "/accounts/**").hasAuthority("SCOPE_account.delete"))
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
//                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
                .authorizeHttpRequests(req -> req.requestMatchers(
                                "/v3/api-docs/**",
                                "/swagger-ui/**",
                                "/swagger-ui**",
                                "/api-docs-ui.html",
                                "/oauth2-redirect.html",
                                "/swagger-ui/oauth2-redirect.html",
                                "/public/**",
                                "/api-docs",
                                "/api-docs/**",
                                "/webjars/**",
                                "/swagger-resources/**",
                                "/actuator/**")
                        .permitAll());

        return httpSecurity.build();
    }
    // @formatter:on
    
}

我通过邮递员成功测试了authorization_code流程(以及PKCE),但是在从Auth-Server检索authorization_code时,我没有运气使用Swagger-UI。当尝试使用授权码检索access_token时,我遇到了以下错误:

Access to fetch at 'http://192.168.0.4:8001/oauth2/token' from origin 'http://192.168.0.4:8002' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

我期望从授权服务器获取access_token:

curl --location 'http://192.168.100.102:8001/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic Y2xpZW50OnNlY3JldA==' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=client' \
--data-urlencode 'redirect_uri=http://192.168.0.4:8002/swagger-ui/oauth2-redirect.html' \
--data-urlencode 'code=S-yW9-fNexepByQEvWnr5MLMSO8YAtX1npBp7VUEF3FAo-YqljVbVGGuf1eIa8kpJnviZXGzsgM59HJlULJc5nHI1blWbcxG7xINm7FpVkcG0zxQKdWBUSxsADy23bKD'
swagger swagger-ui openapi spring-authorization-server spring-resource-server
1个回答
0
投票

CORS 是一个常见问题,许多堆栈溢出问题都涉及它。话虽如此,如果您需要允许来自资源服务器 (Swagger UI) 提供的页面的预检请求,则需要使用 Spring Security 配置 CORS。指南如何:使用带有 PKCE 的单页应用程序进行身份验证包含一个示例配置,您可以使用它来开始使用。

© www.soinside.com 2019 - 2024. All rights reserved.