Angular 2 Spring Security CSRF 令牌

Posted

技术标签:

【中文标题】Angular 2 Spring Security CSRF 令牌【英文标题】:Angular 2 Spring Security CSRF Token 【发布时间】:2017-09-07 01:52:23 【问题描述】:

大家好,我在为我的应用设置安全解决方案时遇到问题! 所以我有一个 REST API 后端,它在 http://localhost:51030 上运行并使用 Spring Framework 开发,而对于前端,我有一个 Angular 2 应用程序(最新版本 A.K.A. Angular 4),它在 http://localhost:4200 上运行。 我已经在后端设置了 CORS 配置,如下所示:

public class CORSFilter implements Filter

// The list of domains allowed to access the server
private final List<String> allowedOrigins = Arrays.asList("http://localhost:4200", "http://127.0.0.1:4200");

public void destroy()




public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
   
    // Lets make sure that we are working with HTTP (that is, against HttpServletRequest and HttpServletResponse objects)
    if (req instanceof HttpServletRequest && res instanceof HttpServletResponse)
    
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // Access-Control-Allow-Origin
        String origin = request.getHeader("Origin");
        response.setHeader("Access-Control-Allow-Origin", allowedOrigins.contains(origin) ? origin : "");
        response.setHeader("Vary", "Origin");

        // Access-Control-Max-Age
        response.setHeader("Access-Control-Max-Age", "3600");

        // Access-Control-Allow-Credentials
        response.setHeader("Access-Control-Allow-Credentials", "true");

        // Access-Control-Allow-Methods
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");

        // Access-Control-Allow-Headers
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, " + CSRF.REQUEST_HEADER_NAME); // + CSRF.REQUEST_HEADER_NAME
    
    chain.doFilter(req, res);



public void init(FilterConfig filterConfig)




使用此配置只能正常工作,我可以执行从角度应用程序到回弹的请求并获得响应并执行任何操作。 但是当我尝试设置 CSRF 安全解决方案时,没有任何效果。 这是在后端设置的 CSRF 和安全配置:

public class CSRF


     /**
     * The name of the cookie with the CSRF token sent by the server as a response.
     */
     public static final String RESPONSE_COOKIE_NAME = "XSRF-TOKEN"; //CSRF-TOKEN

     /**
      * The name of the header carrying the CSRF token, expected in CSRF-protected requests to the server.
      */
    public static final String REQUEST_HEADER_NAME = "X-XSRF-TOKEN"; //X-CSRF-TOKEN

    // In Angular the CookieXSRFStrategy looks for a cookie called XSRF-TOKEN 
    // and sets a header named X-XSRF-TOKEN with the value of that cookie.

    // The server must do its part by setting the initial XSRF-TOKEN cookie 
    // and confirming that each subsequent state-modifying request includes 
    // a matching XSRF-TOKEN cookie and X-XSRF-TOKEN header.



public class CSRFTokenResponseCookieBindingFilter extends OncePerRequestFilter


    protected static final String REQUEST_ATTRIBUTE_NAME = "_csrf";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException
    
        CsrfToken token = (CsrfToken) request.getAttribute(REQUEST_ATTRIBUTE_NAME);

        Cookie cookie = new Cookie(CSRF.RESPONSE_COOKIE_NAME, token.getToken());
        cookie.setPath("/");

        response.addCookie(cookie);

        filterChain.doFilter(request, response);
    


@Configuration
public class Conf extends WebMvcConfigurerAdapter

    @Bean
    public CORSFilter corsFilter()
    
        return new CORSFilter();
    

    @Override
    public void addViewControllers(ViewControllerRegistry registry)
    
        registry.addViewController("/login");
        registry.addViewController("/logout");
    


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter


    @Autowired
    private RESTAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private RESTAuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private RESTAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private RESTLogoutSuccessHandler logoutSuccessHandler;

    @Resource
    private CORSFilter corsFilter;

    @Autowired
    private DataSource dataSource;


    @Autowired
    public void globalConfig(AuthenticationManagerBuilder auth) throws Exception
    
        auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery("select login as principal, password as credentials, true from user where login = ?")
            .authoritiesByUsernameQuery("select login as principal, profile as role from user where login = ?")
            .rolePrefix("ROLE_");
    


    @Override
    protected void configure(HttpSecurity http) throws Exception
    
        //csrf is disabled for the moment
        //http.csrf().disable();

        //authorized requests
        http.authorizeRequests()
            .antMatchers("/api/users/**").permitAll()
            .antMatchers(HttpMethod.OPTIONS , "/*/**").permitAll()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated();

        //handling authentication exceptions
        http.exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint);

        //login configuration
        http.formLogin()
            .loginProcessingUrl("/login")
            .successHandler(authenticationSuccessHandler);
        http.formLogin()
            .failureHandler(authenticationFailureHandler);

        //logout configuration
        http.logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(logoutSuccessHandler);

        //CORS configuration
        http.addFilterBefore(corsFilter, ChannelProcessingFilter.class);


        //CSRF configuration
        http.csrf().requireCsrfProtectionMatcher(
                new AndRequestMatcher(
                // Apply CSRF protection to all paths that do NOT match the ones below

                // We disable CSRF at login/logout, but only for OPTIONS methods to enable the browser preflight
                new NegatedRequestMatcher(new AntPathRequestMatcher("/login*/**", HttpMethod.OPTIONS.toString())),
                new NegatedRequestMatcher(new AntPathRequestMatcher("/logout*/**", HttpMethod.OPTIONS.toString())),

                new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.GET.toString())),
                new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.HEAD.toString())),
                new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.OPTIONS.toString())),
                new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.TRACE.toString()))
            )
        );

        // CSRF tokens handling
        http.addFilterAfter(new CSRFTokenResponseCookieBindingFilter(), CsrfFilter.class);

    


问题出在前端和angular 4配置,CSRF文档太差,网上也没有完整的CSRF实现例子。 所以下面是我的登录服务:

@Injectable()
export class LoginService 

    private loginUrl = 'http://localhost:51030/login';

    constructor(private http: Http) 

    preFlight() 
        return this.http.options(this.loginUrl);
    

    login(username: string , password: string) 

        let headers = new Headers();

        headers.append('Content-Type', 'application/x-www-form-urlencoded');

        let options = new RequestOptions(headers: headers);

        let body = "username="+username+"&password="+password;

        return this.http.post(this.loginUrl , body , options);

    


在登录组件中,我在 ngOnInit 生命周期钩子中执行选项请求:

@Component(
    templateUrl: './login-layout.component.html'
)
export class LoginLayoutComponent implements OnInit 

    credentials = username: '' , password: '';

    constructor(private loginService: LoginService)

    ngOnInit() 
        this.loginService.preFlight()
                         .subscribe();
    

    login() 
        this.loginService.login(this.credentials.username , this.credentials.password)
                         .subscribe(
                            response=>
                                console.log(response) ; 
                            ,error=>
                                console.log(error);
                            
                         );
    



预检进展顺利,我在选项请求中获得了 200 OK 状态以及临时 JSEEIONID 和 XSRF-TOKEN Cookie。

所以在我的应用程序模块中,我按照 Angular 文档中的说明添加了这个:


    provide: XSRFStrategy,
    useValue: new CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN')
  ,

但是,当我尝试使用凭据执行 POST 请求或返回任何请求时,我得到 403 Forbidden:“无法验证提供的 CSRF 令牌,因为找不到您的会话。”

所以请问我该如何解决这个问题,任何人都可以指出正确的方向吗,因为我不知道如何使这项工作! 谢谢!!!

【问题讨论】:

【参考方案1】:

我很惊讶你为 CSRF 和 CORS 做这么多工作,因为 Spring Security 和 Angular 内置了支持。Spring Security 默认启用了 CSRF。

spring 安全手册有很好的关于配置 csrf 的文档: https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#csrf

谷歌搜索“Angular 2 Spring Security csrf”给出了几个例子(以及我是如何找到你的帖子的)。这是一个:

https://medium.com/spektrakel-blog/angular2-and-spring-a-friend-in-security-need-is-a-friend-against-csrf-indeed-9f83eaa9ca2e

【讨论】:

【参考方案2】:

要解决spring security和angular之间的csrf问题,你必须这样做。

在SecurityConfiguration(WebSecurityConfig)中,替换http.csrf().disable();由

               http.csrf()
                .ignoringAntMatchers ("/login","/logout")
                .csrfTokenRepository (this.getCsrfTokenRepository());


    
    private CsrfTokenRepository getCsrfTokenRepository() 
        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        tokenRepository.setCookiePath("/");
        return tokenRepository;

默认的angular csrf拦截器并不总是有效。所以你必须实现自己的拦截器。

import Injectable, Inject from '@angular/core';
import HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler,
  HttpEvent from '@angular/common/http';
import Observable from "rxjs";


@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor 

  constructor(private tokenExtractor: HttpXsrfTokenExtractor) 
  

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> 

    let requestMethod: string = req.method;
    requestMethod = requestMethod.toLowerCase();

    if (requestMethod && (requestMethod === 'post' || requestMethod === 'delete' || requestMethod === 'put')) 
      const headerName = 'X-XSRF-TOKEN';
      let token = this.tokenExtractor.getToken() as string;
      if (token !== null && !req.headers.has(headerName)) 
        req = req.clone(headers: req.headers.set(headerName, token));
      
    

    return next.handle(req);
  

最后将其添加到您的提供程序中 (app.module.ts)

providers: [ provide: HTTP_INTERCEPTORS, useClass: HttpXsrfInterceptor, multi: true ]

考虑导入您的导入。

   HttpClientXsrfModule.withOptions(
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-CSRF-TOKEN'
    ),

【讨论】:

为登录路由禁用 csrf 是否安全?

以上是关于Angular 2 Spring Security CSRF 令牌的主要内容,如果未能解决你的问题,请参考以下文章

Angular 2,Spring Security:跨源 POST 不会发送 cookie

Angular 2 + CORS + 带有 Spring Security 的 Spring Boot Rest Controller

带有 Spring Security 的 Angular 2 应用程序并从 Spring REST 服务中获取数据

Spring Security 的 POST 响应标头中未设置 Angular 2 CSRF cookie

无法使用 curl 通过 tut-spring-security-and-angular-js “pairs-oauth2” 示例调用资源

Spring Boot 2 和 Security With JWT 无法提供 Angular 构建的静态内容