Spring 启动未收到从 Angular 客户端添加的请求 headers

Spring boot doesn't receive request headers added from Angular client

在我的演示应用程序中,我没有收到从 Angular 客户端添加到我的 Spring 启动服务器的请求 headers。作为安全措施,我有 SSL(安全套接字层)和 CORS(跨源资源共享)配置。在我的应用程序中,CSRF(Cross-Site 请求伪造)被禁用。我使用 JWT(JSON Wen Token)作为每个请求的用户认证机制。这就是我需要从 header 中提取 JWT 密钥的地方。我将从我的应用程序中添加代码示例,请帮助我找出问题所在。

Angular拦截器

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpResponse, HttpHandler, HttpRequest, HttpHeaders } from '@angular/common/http'; 
import { UserService } from './user.service';
import { Observable } from 'rxjs';

/*Interceptor
  Often you’ll want to intercept HTTP requests or responses before they’re handled else where in the application*/ 

@Injectable({
  providedIn: 'root'
})
export class InterceptorService implements HttpInterceptor {

  /**
   * Constructor of InterceptorService. 
   * @param userService UserService
   */
  constructor(private userService: UserService) { }

  /**
   * Responsible for intercepting requests. 
   * Responsible for adding 'Authorization' header with JWT (Json Web Token). 
   * @param theRequest HttpRequest<any>
   * @param handler HttpHandler
   */
  intercept(theRequest: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {
    debugger;
    const jwtKey = this.userService.getJwtString();
    const authReq = theRequest.clone({
      headers: new HttpHeaders({
        'Content-Type':  'application/json',
        'Authorization': `Bearer ${jwtKey}`
      })
    });
    console.log('Intercepted HTTP call', authReq);
    return handler.handle(authReq);
  }

}

Angular用户服务

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Router } from '@angular/router';
import { AuthenticationConfigurationService } from './authentication-configuration.service';
import { User, AuthenticationResponse } from './data';

//Service for users 

@Injectable({
  providedIn: 'root'
})
export class UserService {
    //Attributes
  private jwtString: string = ''; //JWT (Jason Web Token)
  private isUserLoggedIn = false; 

  /**
   * Constructor of UserService. 
   * @param router Router
   * @param httpClient HttpClient
   */
  constructor(private router: Router, private httpClient: HttpClient) { }

  /**
   * Executes upon user login. 
   * @param theUser User 
   */
  login(theUser: User) {
    const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) };
      this.httpClient.post(AuthenticationConfigurationService.getAuthenticationURL('authenticationURL') + '/getjwt', theUser, httpOptions)
                .subscribe((response: AuthenticationResponse) => {
                    this.jwtString = response.jwt;
          this.isUserLoggedIn = true;
                });
  }

  /**
   * Executes upon user logout. 
   */
  logout() {;
    this.isUserLoggedIn = false; 
    this.jwtString = '';
    this.navigateToLoginPage();
  }

  /**
   * Responsible for returning the JWT (Json Web Token) string.
   * @returns string
   */
  getJwtString(): string {
    return this.jwtString; 
  }

  /**
   * Responsible for returning whether the user is logged-in. 
   * @returns boolean
   */
  userLoggedIn(): boolean {
    return this.isUserLoggedIn;
  }

  /**
   * Navigate to the login page. 
   */
  private navigateToLoginPage() {
    this.router.navigateByUrl('/login');
  }

}

Spring 启动安全

package com.example.LibraryServer.Security;

import com.example.LibraryServer.Filter.Security.JwtRequestFilter;
import com.example.LibraryServer.Services.LibraryUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

/**
 * Class responsible for security configurations.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //Attributes
    private final LibraryUserDetailsService libraryUserDetailsService;
    private final JwtRequestFilter jwtRequestFilter;

    /**
     * Constructor of SecurityConfig.
     * @param theLibraryUserDetailsService LibraryUserDetailsService
     * @param theJwtRequestFilter JwtRequestFilter
     */
    @Autowired
    public SecurityConfig(LibraryUserDetailsService theLibraryUserDetailsService, JwtRequestFilter theJwtRequestFilter) {
        this.libraryUserDetailsService = theLibraryUserDetailsService;
        this.jwtRequestFilter = theJwtRequestFilter;
    }

    /**
     * Responsible for user security configuration.
     * Overridden from WebSecurityConfigurerAdapter level.
     * @param theHttpSecurity HttpSecurity
     * @throws Exception - Exception upon security configuration.
     */
    @Override
    protected void configure(HttpSecurity theHttpSecurity) throws Exception {
        //theHttpSecurity.authorizeRequests()
                //.antMatchers("/**").access("permitAll") //Allow all paths
                /*.and().cors()*/
                //.and().csrf().disable(); //Allow all requests - CSRF (Cross-Site Request Forgery)
        theHttpSecurity.csrf().disable() //Allow all requests - CSRF (Cross-Site Request Forgery)
                .authorizeRequests().antMatchers("/authenticate/**").access("permitAll") //Allow all paths
                .anyRequest().authenticated() //All other paths need authentication
                /*Inform spring security not to manage sessions.
                  All requests will be filtered via 'JwtRequestFilter' with JWT and does not need sessions.*/
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //Inform spring security about the filter 'JwtRequestFilter' for username and password authentication.
        theHttpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * Responsible for configuring user-store.
     * Overridden from WebSecurityConfigurerAdapter level.
     * @param theAuthentication AuthenticationManagerBuilder
     * @throws Exception - Exception upon user store creation.
     */
    @Override
    public void configure(AuthenticationManagerBuilder theAuthentication) throws Exception {
        //theAuthentication.inMemoryAuthentication()
                //.withUser("sankalpa")
                //.password("{noop}123")
                //.authorities("ROLE_USER");
        theAuthentication.userDetailsService(libraryUserDetailsService);
    }

    /**
     * Method constructing AuthenticationManager bean.
     * This method is needed since AuthenticationManager is being used in 'HelloController'.
     * Therefore this bean should be in spring application context.
     * Overridden from WebSecurityConfigurerAdapter level.
     * @return AuthenticationManager
     * @throws Exception - Exception upon execution.
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * Method constructing a password encoder bean.
     * Constructs 'NoOpPasswordEncoder'.
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

}

Spring 启动 CORS 配置 class

package com.example.LibraryServer.CORS;

import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Configuration class which is responsible for handling CORS.
 * Cross-Origin Resource Sharing (CORS) configuration class.
 */
@Configuration
public class CorsConfiguration implements WebMvcConfigurer {

    /**
     * Responsible for CORS mapping.
     * Overridden from WebMvcConfigurer level.
     * @param theRegistry CorsRegistry
     */
    @Override
    public void addCorsMappings(@NotNull CorsRegistry theRegistry) {
        //End points
        var authorizedEndpoints = new String[] {
                "/book/**",
                "/author/**",
                "/authenticate/**"
        };

        //Add mapping
        for (var endPoint : authorizedEndpoints) {
            theRegistry.addMapping(endPoint)
                    .allowedOrigins("http://localhost:4200")
                    .allowedMethods("*") //"HEAD", "GET", "PUT", "POST", "DELETE", "PATCH"
                    .allowedHeaders("*")
                    .allowCredentials(true)
                    .exposedHeaders("Authorization");
        }
    }

}

Spring 引导过滤器

package com.example.LibraryServer.Filter.Security;

import com.example.LibraryServer.Services.LibraryUserDetailsService;
import com.example.LibraryServer.Uilities.JsonWebTokenUtility;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
 * Filter class for intercepting all the requests and validate with JWT (Jason Web Token).
 */
@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    //Attributes
    private final LibraryUserDetailsService libraryUserDetailsService;
    private final JsonWebTokenUtility jsonWebTokenUtility;

    /**
     * Constructor of JwtRequestFilter.
     * @param theLibraryUserDetailsService LibraryUserDetailsService
     * @param theJsonWebTokenUtility JsonWebTokenUtility
     */
    @Autowired
    public JwtRequestFilter(LibraryUserDetailsService theLibraryUserDetailsService, JsonWebTokenUtility theJsonWebTokenUtility) {
        this.libraryUserDetailsService = theLibraryUserDetailsService;
        this.jsonWebTokenUtility = theJsonWebTokenUtility;
    }

    /**
     * Responsible for intercepting the request via filter and do the validations and needful with JWT.
     * Overridden from OncePerRequestFilter level.
     * @param theRequest HttpServletRequest
     * @param theResponse HttpServletResponse
     * @param theChain FilterChain
     * @throws ServletException - Exception upon execution.
     * @throws IOException - Exception upon execution.
     */
    @Override
    protected void doFilterInternal(HttpServletRequest theRequest, HttpServletResponse theResponse, FilterChain theChain)
            throws ServletException, IOException {
        printLog("doFilterInternal() -> Executed");
        //Get the 'Authorization' from the request header. JWT is suppose to send in request header under 'Authorization'.
        final String authorizationHeader = theRequest.getHeader("Authorization");

        //Method local attributes
        String username = null;
        String jwt = null;

        //Get the JWT String and username out from 'authorizationHeader'
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            printLog("doFilterInternal() -> Extracting the JWT token from the authorization header");
            jwt = authorizationHeader.substring(7);
            username = jsonWebTokenUtility.extractUserName(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            printLog("doFilterInternal() -> User name found: " + username + " and there is no current logged-in user");
            //If the username is not null and there is no current user authenticated

            //Get the user from user details service
            UserDetails userDetails = this.libraryUserDetailsService.loadUserByUsername(username);

            if (jsonWebTokenUtility.validateToken(jwt, userDetails)) {
                printLog("doFilterInternal() -> Load user for the username: " + userDetails.getUsername() +
                        " and validated the JWT successfully");
                //Validating user with the JWT String is successful

                //Create token
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                printLog("doFilterInternal() -> Created 'UsernamePasswordAuthenticationToken'");
                //Set the information to the token
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(theRequest));
                printLog("doFilterInternal() -> Set details to the 'UsernamePasswordAuthenticationToken'");
                //Set the authorized user to the context
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                printLog("doFilterInternal() -> Set 'UsernamePasswordAuthenticationToken' to the 'SecurityContextHolder'");
            }
        }
        //Continue the chain
        printLog("doFilterInternal() -> Continue the chain...");
        theChain.doFilter(theRequest, theResponse);
    }

    /**
     * Responsible for printing main log messages to the console.
     * @param theLogMessage String
     */
    private void printLog(String theLogMessage) {
        log.info("JwtRequestFilter: " + theLogMessage);
    }

}

在上面的过滤器中,我想从 header 中得到 'Authorization',但是请求根本没有 header。当我通过邮递员做同样的事情时,它工作得很好。当我通过 Angular 客户端执行此操作时会发生这种情况。

调试

有同样的问题。在我的例子中,我在预检调用中遇到了 cors 错误。 添加 http.cors() 如下解决了我的问题。

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {



    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.headers().cacheControl();

        http.csrf().disable()
                .authorizeRequests()
                .....;
        http.cors();
    }

    
}