我得到了一个基本的Vue.js应用程序,用于在我们的Spring Boot身份验证服务器上进行身份验证.下面是执行完整登录(重定向到/LOGIN、请求令牌端点等)的代码:

<template>
    <div class="h-screen w-screen flex flex-col items-center justify-center">
        <p class="text-center mb-5">You need to be logged in<br>to access the admin dashboard.</p>
        <button @click.prevent="startAuthFlow" class="bg-blue-500 text-white h-8 w-36 rounded shadow">Login</button>
    </div>
</template>

<script setup>
import { config 
} from "./../common/config.js";
import { parseQueryString, generateR和omString } from "./../utils/authHelpers.js";
import router from './../router';
import { useAuthStore } from './../stores/auth.js';

const auth = useAuthStore();

if (auth.access_token) {
    router.push({ name: 'home', replace: true });
}

const startAuthFlow = () => {
    console.log("Starting auth flow...");

    // Create 和 store a r和om "state" value
    var state = generateR和omString();
    localStorage.setItem("pkce_state", state);
    console.log(state);

    // Build the authorization URL
    var url = config.authorization_endpoint
        + "?response_type=code"
        + "&client_id=" + encodeURIComponent(config.client_id)
        + "&state=" + encodeURIComponent(state)
        + "&redirect_uri=" + encodeURIComponent(config.redirect_uri);

    // Redirect to the authorization server
    window.location = url;
};

// H和le the redirect back from the authorization server 和
// get an access token from the token endpoint
var q = parseQueryString(window.location.search.substring(1));

// Check if the server returned an error string
if (q.error) {
    alert("Error returned from authorization server: " + q.error);
}

// If the server returned an authorization code, attempt to exchange it for an access token
if (q.code) {
    // Verify state matches what we set at the beginning
    if (localStorage.getItem("pkce_state") != q.state) {
        alert("Invalid state");
    } else {
        // Base64 encode client credentials
        const base64Credentials = btoa(config.client_id + ':' + config.client_secret);

        // Build the token URL
        var url = config.token_endpoint
            + "?grant_type=authorization_code"
            + "&client_id=" + encodeURIComponent(config.client_id)
            + "&code=" + q.code
            + "&redirect_uri=" + encodeURIComponent(config.redirect_uri);

        // Send POST request to token endpoint to retrieve access token
        fetch(url, {
            method: "POST",
            headers: {
                "Authorization": "Basic " + base64Credentials,
                'Content-Type': 'application/x-www-form-urlencoded',
            }
        }).then(response => response.json())
            .then(result => {
                console.log(result);
                // Extracting tokens from result
                const { access_token, refresh_token } = result;

                // Save login to pinia 和 tokens to cookies
                auth.login({ access_token, refresh_token, user: null });

                router.push({ name: 'home', replace: true });
            })
            .catch(error => console.log('error', error));
    }

    // Clean up local storage
    localStorage.removeItem("pkce_state");
}

</script>

当使用带有停用的安全功能的浏览器时(not次判断COR),这完全可以正常工作.无论是登录还是其他请求.

现在,当使用普通浏览器时,重定向到/login完全可以正常工作.但是,当向token端点发送请求时,出现以下错误:

获取位置的访问权限 ‘http://localhost:8081/oauth2/token?grant_type=authorization_code&amp;client_id=core-server&amp;code=Ps19gIUUePThDLr15xX0U-UWEMd0HgHfyAOCcZjGmfXUqED80GOMLBykuldNrL7k23dxEydcP49hX_kGigKsZjcFCTS93xU7kwdwAIm6-eIcIk_ayN5i0mZPLeg_bDYx&amp;redirect_uri=http%3A%2F%2Flocalhost%3A5173%2F’ 发端的http://localhost:5173‘已被CORS策略拦截: 对印前判断请求的响应未通过访问控制判断:否 "Access-Control-Allow-Origin"标头位于请求的 资源.如果不透明的响应满足您的需求,请设置请求的 将模式设置为‘no-CORS’可在禁用CORS的情况下获取资源.

在停用CORS的浏览器中,我得到了以下标头:

Vary: Origin 
Vary: Access-Control-Request-Method 
Vary: Access-Control-Request-Headers

以下是我的SecurityConfig文件以及CorsConfig:

@Configuration
public class
CorsConfig {

    private static final Logger logger = LoggerFactory.getLogger(CorsConfig.class);

    /**
     * Cors configuration
     */
    @Bean(name="corsConfigurationSource")
    CorsConfigurationSource corsConfigurationSource() {

        logger.info("Creating corsConfigurationSource bean");

        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of(
                "http://localhost:5173",
                "http://192.168.2.144:5173"
        ));

        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(List.of(
                "Authorization",
                "Content-Type",
                "Accept",
                "Origin",
                "X-Requested-With"
        ));
        configuration.setExposedHeaders(List.of(
                "Cache-Control",
                "Content-Language",
                "Content-Type",
                "Expires",
                "Last-Modified",
                "Pragma"
        ));
        configuration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // claim names used in the bearer token
    private static final String ROLES_CLAIM = "user-authorities";
    private static final String SCOPES_CLAIM = "scope";

    private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);


    @Bean
    @Order(1)
    public CorsFilter corsFilter(CorsConfigurationSource corsConfigurationSource) {
        logger.info("Creating corsFilter bean");
        return new CorsFilter(corsConfigurationSource);
    }


    /**
     * Configures the authorization server endpoints.
     */
    @Bean
    @Order(2)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, RegisteredClientRepository clientRepository) throws Exception {

        logger.info("Creating authorizationServerSecurityFilterChain bean");

        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .registeredClientRepository(clientRepository) // autowired from ClientConfig.java
                .oidc(Customizer.withDefaults());

        http.exceptionH和ling((exceptions) -> exceptions
            .defaultAuthenticationEntryPointFor(
                new LoginUrlAuthenticationEntryPoint("/login"),
                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
            )
        );

        http.oauth2ResourceServer((resourceServer) -> resourceServer
                .jwt(Customizer.withDefaults()));

        http.csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }


    /**
     * Secures pages used to log in, log out, register etc.
     * Sets custom login menu.
     */
    @Bean
    @Order(3)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/admin/**")));

        logger.info("Creating defaultSecurityFilterChain bean");

        http.authorizeHttpRequests((authorize) ->
                authorize
                        .requestMatchers(new AntPathRequestMatcher("/register")).permitAll()
                        .requestMatchers(new AntPathRequestMatcher("/recover/**")).permitAll()
                        .requestMatchers(new AntPathRequestMatcher("/error/**")).permitAll()
                        .requestMatchers(new AntPathRequestMatcher("/css/**")).permitAll()
                        .requestMatchers(new AntPathRequestMatcher("/js/**")).permitAll()
                        .requestMatchers(new AntPathRequestMatcher("/favicon.ico")).permitAll()
                        .anyRequest().authenticated());

        http.oauth2ResourceServer((resourceServer) -> resourceServer
                .jwt(Customizer.withDefaults()));

        // set custom login form
        http.formLogin(form -> {
            form.loginPage("/login");
            form.permitAll();
        });

        http.logout(conf -> {
            // default logout url
            conf.logoutSuccessH和ler(logoutSuccessH和ler());
        });

        // Temp disable CSRF
        http.csrf(AbstractHttpConfigurer::disable);
        http.cors(AbstractHttpConfigurer::disable);

        return http.build();
    }


    /**
     * Secures admin endpoints with a bearer token. Does not use session authentication.
     */
    @Bean
    @Order(4)
    public SecurityFilterChain adminResourceFilterChain(HttpSecurity http) throws Exception {

        logger.info("Creating adminResourceFilterChain bean");

        // h和le out custom endpoints in this filter chain
        http.authorizeHttpRequests((authorize) ->
                authorize
                        .requestMatchers(new AntPathRequestMatcher("/admin/**")).hasRole("ADMIN")
                        .anyRequest().authenticated());

        http.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.oauth2ResourceServer((resourceServer) -> resourceServer
                .jwt(Customizer.withDefaults()));

        // Temp disable CSRF
        http.csrf(AbstractHttpConfigurer::disable);
        http.cors(AbstractHttpConfigurer::disable);


        return http.build();
    }

// ...

我try 了很多不同的方法:

  • 更改了筛选器链(例如,我在每个安全Bean的顶部添加了CORS配置,而不是Order(1))
  • Went ahead 和 implemented the CORS Tutorial one by one.
  • 已try 手动设置请求标头(which isn't a solution in the long-term).
  • try 模式匹配以临时允许每个传入请求(不起作用,因此这显然是CORS配置本身的问题)

推荐答案

S单一P年龄A应用程序(Angular 、react 、Vue.js等)以及移动应用程序不应是OAuth2客户端.这类客户是"公共"客户和this is now discouraged家.

你的Vue应用程序应该使用OAuth2登录的BFF上的会话来保护,并使用包含访问令牌的授权头来替换会话Cookie.要实现这一点,最简单的方法可能是对TokenRelay过滤器使用spring-cloud-gateway,对oauth2Login使用spring-boot-starter-oauth2-client,就像我在this tutorial中所做的那样.本教程中的前端使用的是Angular ,但登录和注销是纯Typsecript代码,很容易移植到Reaction或Vue.

除了使用会话保护前端之外,网关还可以消除大多数CORS配置的需要:从浏览器的Angular 来看,通过网关路由的所有请求都具有相同的来源(网关).

在链接的教程中:

  • 路径以/ui/开头的对网关(比方说https://localhost:8080)的所有请求都被路由到为UIassets资源 提供服务的服务器(类似于Angular 开发服务器中的https://localhost:4200/ui/,但可以是Vue开发服务器、包含任何内容的ngix实例等).
  • 通过路径以/bff/v1/开头的网关的所有请求都被路由到资源服务器(类似于https://localhost:7084/)

在上面的配置中,如果用户将其浏览器指向https://localhost:8080/ui/,并且如果将SPA配置为向https://localhost:8080/bff/v1/**,发送REST请求,则从浏览器的Angular 来看,对UI和对API的请求都以https://localhost:8080为来源.

同样在本教程中,授权服务器不通过网关进行路由,并且需要一些CORS配置来允许以网关为源的请求.最常见的原因是,在使用OAuth2时,您最感兴趣的是SingSigOn:在不同的应用程序之间共享相同的授权服务器,以使用户在使用相同的浏览器时不必多次进行身份验证,这需要使用相同的Cookie,因此无论BFF实例如何(而不是像https://localhost:8080/auth这样的东西,例如在教程BFF的情况下),都需要使用相同的主机和端口(授权服务器由浏览器联系,假设是https://oidc.c4-soft.com).

总而言之,如果使用网关作为OAuth2机密客户端,您可以消除对CORS配置的需要:

  • 来自资源服务器(如果您通过网关路由UI和REST请求)
  • 来自授权服务器(如果您通过网关来路由UI和授权服务器,但代价是失go SSO)

Java相关问答推荐

收听RDX中用户数据的变化

如何从片段请求数据到活动?在主要活动中单击按钮请求数据?

如何用javac编译Java类,即使对像java.lang.*这样的基本类也没有任何依赖关系?

Java事件系统通用转换为有界通配符

无法找到符号错误—Java—封装

@从类文件中删除JsonProperty—Java

如何使用jooq generator将表名和列名映射为人类可读的?

ittext pdf延迟签名,签名无效

使用标记时,场景大纲不在多个线程上运行

根据对象和值的参数将映射<;T、值&>转换为列表<;T&>

Exe4j创建的应用程序无法再固定在任务栏Windows 11上

呈现文本和四舍五入矩形时出现的JavaFX窗格白色瑕疵

Bean定义不是从Spring ApplationConext.xml文件加载的

是否为计划任务补偿系统睡眠?

Regex以查找不包含捕获组的行

从LineChart<;字符串、字符串和gt;中删除数据时出现特殊的ClassCastException;

Domino中不同的java.Protocol.handler.pkgs设置在XPages Java中导致错误

RestTemplate Bean提供OkHttp3ClientHttpRequestFactory不支持Spring Boot 3中的请求正文缓冲

TinyDB问题,无法解析符号';上下文&

为什么当我输入变量而不是直接输入字符串时,我的方法不起作用?