java - 为什么 Angular 不通过 POST 请求发送 cookie,即使我已将 withCredentials 设置为 true?

标签 java angular spring-boot authentication spring-security

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

How can I fix Angular and Spring Boot configuration with Spring JDBC authentication so that I am able to logout even with CSRF protection enabled?

对/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 token 。所以我最终得到了 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)
        })
      );
  }
}

最佳答案

在 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 })

解决了问题。

关于java - 为什么 Angular 不通过 POST 请求发送 cookie,即使我已将 withCredentials 设置为 true?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60082514/

相关文章:

java - 在 Java 中格式化 tempTextField。

angular - 在子路由 Angular 2 之间导航

angular - 导入共享 Angular 模块 - 错误模块在本地声明组件,但未导出

spring-boot - java.lang.IllegalStateException : Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead 错误

java - 如何在 Spring Boot API 测试中模拟 Spring 5 WebClient

java - Java中SocketRead0线程挂了,怎么办?

java - Java中相同逻辑的泛化方法

java - 如何创建一个通用的 HashMap 来插入集合和对象?

Angular 6 zip 已弃用 : resultSelector is no longer supported, 管道改为映射

java - 使用 @Async 注释限制线程数并在达到最大线程数时等待