使用 Spring Security 和 AngularJS 预防 CSRF

Posted

技术标签:

【中文标题】使用 Spring Security 和 AngularJS 预防 CSRF【英文标题】:CSRF Prevention with Spring Security and AngularJS 【发布时间】:2020-03-13 15:57:12 【问题描述】:

我使用的是 Spring 4.3.12.RELEASE 版本,AngularJS 1.4.8。我正在尝试阻止对应用程序的 CSRF 攻击。

@Configuration
    @Order(2)
    public static class SecurityConfig extends WebSecurityConfigurerAdapter 

        String[] pathsToRemoveAuthorizaton = 
                "/mobile/**",
                "/logout",
                "/j_spring_security_logout",
                "/login",
        ;

        private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

        @Override
        public void configure(HttpSecurity http) throws Exception 

            logger.info("http configure");
            http.antMatcher("/**").authorizeRequests().antMatchers(pathsToRemoveAuthorizaton).permitAll()
                    .antMatchers("/**").authenticated()
                    .and().formLogin().loginPage("/login")
                    .usernameParameter("employeeId").passwordParameter("password")
                    .successForwardUrl("/dashboard").defaultSuccessUrl("/dashboard", true)
                    .successHandler(customAuthenticationSuccessHandler()).loginProcessingUrl("/j_spring_security_check")
                    .and().logout().logoutSuccessUrl("/logout").logoutUrl("/j_spring_security_logout")
                    .logoutSuccessHandler(customLogoutSuccessHandler()).permitAll().invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID").and().sessionManagement().sessionFixation().newSession()
                    .maximumSessions(1).maxSessionsPreventsLogin(true).and()
                    .sessionCreationPolicy(SessionCreationPolicy.NEVER).invalidSessionUrl("/logout").and()
                    .exceptionHandling().accessDeniedPage("/logout");

//          http.csrf().csrfTokenRepository(csrfTokenRepository()).and()
//          .addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);

            http.csrf().csrfTokenRepository(csrfTokenRepository());

//          http.csrf().disable();
//          http.csrf().ignoringAntMatchers("/mobile/**");

            http.authorizeRequests().anyRequest().authenticated();
        

        private CsrfTokenRepository csrfTokenRepository() 
            HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
            repository.setHeaderName("X-XSRF-TOKEN");
            return repository;
        


        @Bean
        public AuthenticationSuccessHandler customAuthenticationSuccessHandler() 
            return new CustomAuthenticationSuccessHandler();
        

        @Bean
        public LogoutSuccessHandler customLogoutSuccessHandler() 
            return new CustomLogoutSuccessHandler();
        
    

以下是我的角度服务代码

govtPMS.service('Interceptor', function($q, $location, $rootScope, pinesNotifications, Auth) 

  return 

    request: function(config) 
        config.headers.Authorization = 'Bearer '+$rootScope.authToken;
//        document.cookie = 'CSRF-TOKEN=' + $rootScope.generateKey;

       return config;
    ,
    requestError: function (rejection) 
        return $q.reject(rejection);
    ,
    response: function(res) 

        if(res.status === 200 || res.status === 201)
            if(res.data.response !== undefined)
                if(res.data.status === 1 || res.data.status === 3 || res.data.status === 2) 
                    pinesNotifications.notify(
                        'title': 'Success',
                        'text': res.data.message,
                        'type': 'success',
                        'delay': 5000
                    );
                
                 else if(res.data.status === 5) 
                    pinesNotifications.notify(
                        'title': 'Warning',
                        'text': res.data.message,
                        'type': 'warning',
                        'delay': 5000
                    );
                
            
        
      return res || $q.when(res);
    ,
    responseError: function(error) 
      return $q.reject(error);
    
  ;
).config(['$httpProvider', function($httpProvider) 
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
  $httpProvider.defaults.xsrfCookieName = 'CSRF-TOKEN';

  $httpProvider.interceptors.push('Interceptor');

])

我仍然无法看到 CSRF 令牌标头以及请求。

在这个应用程序中,我们使用了 3 个 jsp 页面 - login.jsp、logout.jsp 和 dashboard.jsp 角度范围在dashboard.jsp 中定义,因此登录和注销超出了AngularJS 的范围。 我还尝试了this 和this 示例中的无状态方式,其中 angular 生成 UUID 并附加 cookie 和请求标头,下面的过滤器做得很好。 直到注销攻击。在此攻击中,攻击者试图成功注销用户,因为要从应用程序中注销,我们只是使用了 href。

<li><a href="j_spring_security_logout" ><i class="fa fa-sign-out"></i><span>Logout</span></a></li>

现在,由于它的注销没有角度,所以 angularjs 拦截器无法在此处附加 UUID。 自上周以来,我一直在为此苦苦挣扎,我们将不胜感激。

StatelessCSRFFilter.java

package com.leadwinner.sms.config.filters;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.web.filter.OncePerRequestFilter;

public class StatelessCSRFFilter extends OncePerRequestFilter 



    private static final String CSRF_TOKEN = "CSRF-TOKEN";
    private static final String X_CSRF_TOKEN = "X-CSRF-TOKEN";
    private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException 

        List<String> excludedUrls = new ArrayList<>();
        excludedUrls.add("/resources");
        excludedUrls.add("/j_spring_security_check");
        excludedUrls.add("/j_spring_security_logout");
        excludedUrls.add("/login");
        excludedUrls.add("/logout");
        excludedUrls.add("/mobile");
        excludedUrls.add("/migrate");
        excludedUrls.add("/dashboard");

        String path = request.getServletPath();
        System.out.println(path);

        AtomicBoolean ignoreUrl = new AtomicBoolean(false);

        excludedUrls.forEach(url -> 
            if (request.getServletPath().startsWith(url.toLowerCase()) || request.getServletPath().equals("/")) 
                ignoreUrl.set(true);
            
        );

        if (!ignoreUrl.get()) 
            final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN);
            final Cookie[] cookies = request.getCookies();
            System.out.println("**************************************************");
            System.out.println("--------------------------------------------------");
            String csrfCookieValue = null;
            if (cookies != null) 
                for (Cookie cookie : cookies) 
                    if (cookie.getName().equals(CSRF_TOKEN)) 
                        csrfCookieValue = cookie.getValue();
                    
                
            
            System.out.println("csrfTokenValue = "+csrfTokenValue);
            System.out.println("csrfCookieValue = "+csrfCookieValue);

            System.out.println("--------------------------------------------------");
            System.out.println("**************************************************");
            if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) 
                accessDeniedHandler.handle(request, response, new AccessDeniedException(
                        "Missing or non-matching CSRF-token"));
                return;
            
        
        filterChain.doFilter(request, response);
    

【问题讨论】:

我有点困惑,因为您似乎正在使用 Spring Security 的 CSRF 保护(连接 CsrfTokenRepository),同时还禁用了保护(csrf().disable())。此外,您正在编写自己的 CSRF 过滤器。如果您尝试编写自己的 CSRF 保护,请您澄清一下吗?如果是的话,为什么Spring Security提供的还不够呢? 对不起。已被评论。我只需要使用 Spring 的 CSRF 保护方式,并为某些移动 API 禁用 CSRF。由于我不能这样做,我尝试了另一种通过角度拦截器在 Header 和 Cookie 中附加 CSRF 令牌的替代方法,除了注销页面之外,它工作正常,因为对于注销,href 正在使用。在 Spring 中启用 CSRF 保护的任何帮助,我可以禁用移动 API 的 CSRF 检查就足够了。 【参考方案1】:

如果可以通过浏览器发出请求并且自动提交凭据(会话 cookie、基本身份验证凭据),则 CSRF 保护是必要的,即使使用移动 API。

鉴于您将移动 API 作为应用程序的一部分,问题是浏览器能否成功处理这些 API?

我建议您创建两个单独的过滤器链,就像这样,一个用于网络应用程序,一个用于移动 API:

@Configuration
@Order(100)
public class WebAppConfig extends WebSecurityConfigurerAdapter 
    @Override
    protected void configure(HttpSecurity http) 
        http
            .requestMatchers()
                .antMatchers("/app/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .formLogin();
    


@Configuration
@Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter 
    @Override
    protected void configure(HttpSecurity http) 
        http
            .requestMatchers()
                .antMatchers("/api/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt();
    

这样做的目的是将两种配置分开。与 Web 应用程序相关的端点使用一种设置,而与移动 API 相关的端点使用另一种设置。

现在,有几个关于移动 API 的问题。我假设您正在使用 OAuth 2.0 Bearer 令牌进行身份验证,这就是上面的配置使用 Spring Security 5.1+ 中的 oauth2ResourceServer() 的原因。这样做是有选择地为包含 Authorization: Bearer xyz 标头的请求禁用 CSRF。

但是,由于您使用的是 Spring Security 4.3,因此您可能需要执行以下操作(除非您可以升级):

@Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter 
    @Override
    protected void configure(HttpSecurity http) 
        http
            .requestMatchers()
                .antMatchers("/api/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .sessionManagement().sessionCreationPolicy(NEVER)
                .and()
            .addFilterBefore(new MyMobileAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .csrf().disable();
    

不过,您需要确保的是,您的自定义身份验证过滤器不使用浏览器自动从任何来源(会话 cookie,Authorization: Basic)发送的身份验证机制。

【讨论】:

1.移动 API,我们不包括在此配置中,有一个 MobileSessionAspector 正在拦截我们正在检查 JWT 令牌授权的所有移动相关 API。 2. 我已经在使用 MultiHttpSecurity Config。 ***.com/questions/58231064/… 3.我需要做的就是在 Spring 和 AngularJS 中启用 CSRF,以便保护所有请求(API)和注销功能免受 CSRF 攻击,并且我可以选择指定可以排除的模式列表CSRF 保护。【参考方案2】:

`嗨湿婆,

您在 SecurityConfig 的 configure 方法中的代码应该如下所示:

 http
     .authorizeRequests()
     .antMatchers(patterns)
     .permitAll()
       .antMatchers("/hello/**")
       .hasRole("USER")
       .and()
       .csrf()
       .csrfTokenRepository(csrfTokenRepository())
       .requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
       .and()
       .httpBasic()
       .and()
       .addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
       .addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);

在StatelessCSRFFilter中,使用如下代码:

 @Override
 public void init(FilterConfig filterConfig) throws ServletException 

 @Override
 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException 
     CsrfToken csrf = (CsrfToken) servletRequest.getAttribute(CsrfToken.class.getName());
     String token = csrf.getToken();
     if (token != null && isAuthenticating(servletRequest)) 
         HttpServletResponse response = (HttpServletResponse) servletResponse;
         Cookie cookie = new Cookie("XSRF-TOKEN", token);
         cookie.setPath("/");
         response.addCookie(cookie);
     
     filterChain.doFilter(servletRequest, servletResponse);
 

 private boolean isAuthenticating(ServletRequest servletRequest) 
     HttpServletRequest request = (HttpServletRequest) servletRequest;
     return request.getRequestURI().equals("/login");
 `

【讨论】:

你也可以定义 csrfFilter(patterns) 方法吗?【参考方案3】:

<pre>
	Add this code for the csrfTokenRepository method
	     private CsrfTokenRepository csrfTokenRepository() 
	         HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
	         repository.setHeaderName("X-XSRF-TOKEN");
	         return repository;
	     
	Add this for the csrfFilter method
	     private Filter csrfFilter(String[] patterns) 
	         CsrfFilter csrfFilter = new CsrfFilter(csrfTokenRepository());
	         csrfFilter.setRequireCsrfProtectionMatcher(csrfProtectionMatcher(patterns));
	         return csrfFilter;
	     
	Add this for the csrfProtectionMatcher method
	     private NoAntPathRequestMatcher csrfProtectionMatcher(String[] patterns) 
	         return new NoAntPathRequestMatcher(patterns);
	     
	Also remove these lines in configure method
	       .csrfTokenRepository(csrfTokenRepository())
	       .requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
	Move these lines below .csrf() in configure method:
	       .and()
	       .addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
</pre>

【讨论】:

以上是关于使用 Spring Security 和 AngularJS 预防 CSRF的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Spring Security 模拟身份验证和授权

如何使用 Spring-Security 3 和 Hibernate 4 将 spring security xml 配置 hibernate 转换为 java config

Spring Framework,Spring Security - 可以在没有 Spring Framework 的情况下使用 Spring Security?

使用 Spring 和 Spring Security 正确注入 SessionFactory

如何使用spring-security通过用户名和密码登录?

Spring 框架 4.0 和 Spring security 3.2.4 上的 Spring Security SAML 扩展