Почему Angular не отправляет куки с запросом POST, даже если для свойства withCredentials установлено значение true? - PullRequest
0 голосов
/ 05 февраля 2020

Я отредактировал вопрос, чтобы сделать его более понятным. Оригинал был:

Как исправить Angular и конфигурацию Spring Boot с аутентификацией Spring JDB C, чтобы я мог выйти из системы даже с включенной защитой CSRF?

С отключенной функцией CSRF для / logout:

Я могу войти (получаю файлы cookie CSRF и JSESSIONID) и выйти (получено 200 OK) с помощью Почтальона.

Я могу войти (получение файлов cookie CSRF и JSESSIONID) и выход из системы (получено 200 OK) с использованием Firefox и интерфейса Angular.

При включенном CSRF для / logout:

Я могу войти (получение файлов cookie CSRF и JSESSIONID) и выход из системы (получено 200 OK) с использованием Почтальона.

Я могу войти, используя Firefox и Angular внешний интерфейс.

При попытке войти out, однако ...

Сначала выполняется предполётный запрос:

Затем я вижу запрос в / logout:

Я отладил бэкэнд, и кажется, что Spring не может найти мат hing CSRF токен в своем TokenRepository. В итоге я получаю исключение 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);
    }
}

Фронтенд:

Для / входа в систему и / выхода из системы Я делаю POST-запросы с withCredentials: 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)
        })
      );
  }
}

1 Ответ

0 голосов
/ 06 февраля 2020

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

решило проблему.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...