为啥即使我已将 withCredentials 设置为 true,Angular 也不通过 POST 请求发送 cookie?

Posted

技术标签:

【中文标题】为啥即使我已将 withCredentials 设置为 true,Angular 也不通过 POST 请求发送 cookie?【英文标题】:Why is Angular not sending cookies with a POST request even though I've set withCredentials to true?为什么即使我已将 withCredentials 设置为 true,Angular 也不通过 POST 请求发送 cookie? 【发布时间】:2020-05-21 18:20:29 【问题描述】:

我已编辑问题以使其更有意义。原文是:

如何使用 Spring JDBC 身份验证修复 Angular 和 Spring Boot 配置,以便即使启用 CSRF 保护也能够注销?

为 /logout 禁用 CSRF:

我可以使用 Postman 登录(接收 CSRF 和 JSESSIONID cookie)和注销(收到 200 OK)。

我能够使用 Firefox 和 Angular 前端登录(接收 CSRF 和 JSESSIONID cookie)和注销(收到 200 OK)。

为 /logout 启用 CSRF:

我可以使用 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;
    

RESTAuthenticationEntryPoint:

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);
    

RESTAuthenticationFailureHandler

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);
    

RESTAuthenticationSuccessHandler

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);
    

RESTLogoutSuccessHandler

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: true 发出 POST 请求,并配置了 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)
        )
      );
  

【问题讨论】:

【参考方案1】:

在 HttpClient 中,POST 方法的签名与 GET 略有不同:https://github.com/angular/angular/blob/master/packages/http/src/http.ts

第二个参数是我们要发送的任何请求体,不是选项,它是第三个参数。所以 withCredentials: true 根本就没有在实际请求中正确设置。

将调用更改为:

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

解决了问题。

【讨论】:

以上是关于为啥即使我已将 withCredentials 设置为 true,Angular 也不通过 POST 请求发送 cookie?的主要内容,如果未能解决你的问题,请参考以下文章

为啥即使对于同一个站点 Ajax 请求也需要 XMLHttpRequest.withCredentials

UIScrollView - 无法向下滚动很远(即使我已将 contentsize 设置为 1000)[重复]

当 SLComposeViewController 加载时,即使我已将 Main UIViewController 固定为 Portrait,它也会旋转到设备方向

为啥即使帐户已关联,我也无法将转化事件从 Firebase 导入 AdWords?

即使我将应用程序清单设置为纵向,为啥我的 Android 相机预览仍显示横向?

为啥 View Controller 在 UIScrollView 上方滚动?