为啥即使我已将 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?