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