java 为什么即使我已经将withCredentials设置为true,Angular也不发送带有POST请求的cookie?

syqv5f0l  于 2023-03-06  发布在  Java
关注(0)|答案(1)|浏览(126)

我对这个问题进行了编辑,使其更有意义。原文是:
如何使用Spring JDBC身份验证修复Angular和Spring Boot配置,以便即使启用了CSRF保护也能注销?
禁用CSRF的/logout:
我可以使用Postman登录(接收CSRF和JSESSIONID cookie)和注销(接收200 OK)。
我可以使用Firefox和Angular前端登录(接收CSRF和JSESSIONID cookie)和注销(接收200 OK)。
启用CSRF的/logout:
我可以使用Postman登录(接收CSRF和JSESSIONID cookie)和注销(接收200 OK)。
我可以使用Firefox和Angular前端登录。
但是,尝试注销时...
首先是一个成功的印前检查请求:

然后,我看到一个请求/logout:

我调试了后端,Spring似乎无法在其TokenRepository中找到匹配的CSRF令牌。因此,我以MissingCsrfTokenException和403 Forbidden结束。我该如何修复?
后端:
安全配置:

package org.adventure.configuration;

import org.adventure.security.RESTAuthenticationEntryPoint;
import org.adventure.security.RESTAuthenticationFailureHandler;
import org.adventure.security.RESTAuthenticationSuccessHandler;
import org.adventure.security.RESTLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private RESTAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private RESTAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private RESTAuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private RESTLogoutSuccessHandler restLogoutSuccessHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private AccountsProperties accountsProperties;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests().antMatchers("/h2-console/**")
                .permitAll();
        httpSecurity.authorizeRequests().antMatchers("/secure/**").authenticated();
        httpSecurity.cors().configurationSource(corsConfigurationSource());
        httpSecurity.csrf()
                .ignoringAntMatchers("/h2-console/**")
                .ignoringAntMatchers("/login")
                //.ignoringAntMatchers("/logout")
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        httpSecurity.headers()
                .frameOptions()
                .sameOrigin();
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        httpSecurity.formLogin().successHandler(authenticationSuccessHandler);
        httpSecurity.formLogin().failureHandler(authenticationFailureHandler);
        httpSecurity.logout().logoutSuccessHandler(restLogoutSuccessHandler);
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
                .dataSource(dataSource)
                .withDefaultSchema();

        if (Objects.nonNull(accountsProperties)) {
            FirstUser firstUser = accountsProperties.getFirstUser();
            if (Objects.nonNull(firstUser)) {
                String name = firstUser.getName();
                String password = firstUser.getPassword();
                if (Objects.nonNull(name) && Objects.nonNull(password) &&
                !("".equals(name) || "".equals(password))) {
                    jdbcUserDetailsManagerConfigurer.withUser(User.withUsername(name)
                            .password(passwordEncoder().encode(password))
                            .roles("USER"));
                }
            }
            FirstAdmin firstAdmin = accountsProperties.getFirstAdmin();
            if (Objects.nonNull(firstAdmin)) {
                String name = firstAdmin.getName();
                String password = firstAdmin.getPassword();
                if (Objects.nonNull(name) && Objects.nonNull(password) &&
                    !("".equals(name) || "".equals(password))) {
                    jdbcUserDetailsManagerConfigurer.withUser(User.withUsername(name)
                            .password(passwordEncoder().encode(password))
                            .roles("ADMIN"));
                }
            }
        }
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // Strength increased as per OWASP Password Storage Cheat Sheet
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
        configuration.setAllowedHeaders(List.of("X-XSRF-TOKEN", "Content-Type"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        source.registerCorsConfiguration("/secure/classifieds", configuration);
        source.registerCorsConfiguration("/login", configuration);
        source.registerCorsConfiguration("/logout", configuration);
        return source;
    }
}

REST身份验证入口点:

package org.adventure.security;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

REST验证失败处理程序

package org.adventure.security;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {

        super.onAuthenticationFailure(request, response, exception);
    }
}

REST验证成功处理程序

package org.adventure.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) {
        clearAuthenticationAttributes(request);
    }
}

REST注销成功处理程序

package org.adventure.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class RESTLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) {
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

前端:
要/login和/logout,我使用withCredentials发出POST请求:true,并且配置了HttpInterceptor:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class XsrfInterceptor implements HttpInterceptor {

    constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        let requestToForward = req;
        let token = this.tokenExtractor.getToken() as string;
        if (token !== null) {
            requestToForward = req.clone({ setHeaders: { "X-XSRF-TOKEN": token } });
        }
        return next.handle(requestToForward);
    }
}

注销服务:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class LogoutService {

  private url = 'http://localhost:8080/logout';

  constructor(private http: HttpClient) { }

  public logout(): Observable<any> {
    return this.http.post(
      this.url, { withCredentials: true }).pipe(
        map(response => {
          console.log(response)
        })
      );
  }
}
gcuhipw9

gcuhipw91#

在HttpClient中,POST方法的签名与GET方法稍有不同:https://github.com/angular/angular/blob/master/packages/http/src/http.ts
第二个参数是我们想要发送的任何请求主体,而不是选项,后者是第三个参数。true在实际请求中根本没有正确设置。
将呼叫更改为:

this.http.post(
  this.url, null, { withCredentials: true })

解决了这个问题。

相关问题