Angular 带有 csrf 令牌的 Spring Boot Spring 安全应用程序 Ajax POST 在 Token/Header 未定义时失败

Angular SpringBoot SpringSecurity Application Ajax POST with csrf Token Fails on Token/Header being undefined

上下文/原样

我目前正在开发一个 single-page Angular JS 应用程序,使用 SpringBoot 和 Spring 安全实现。该应用程序适用于 'Study Planner',学生可以在日历中输入学习休假。到目前为止,我们让学生能够登录,并在日历中输入详细信息 client-side。

目标/成为

能够在我们的 client-side 中捕获事件添加之后 javascript,我们正在尝试使用 Ajax POST 将其发送回我们的服务器。一旦发送到服务器,目标就是将其保存在我们的数据库中以供学生查看,以便学生下次查看日历时可以加载它。

问题

我们遇到的问题是围绕 Ajax POST 方法,我相信是由于将 csrf header 引入 POST 以试图通过 Spring 安检。如果没有 csrf header,我会看到 Chrome 内的网络流量并收到 403(未经授权)错误。随着 csrf header 的引入,没有网络流量记录,服务器没有被命中,ajax "error" 函数被命中。

无 CSRF 令牌

  1. 学生登录他们的日历
  2. 学生加入活动
  3. 警报已触发'newEventData function'
  4. 警报已触发'we failed'
  5. 403 Chrome

    错误

    403(禁止) "Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'."

使用 CSRF 令牌

(改编自 spring docs 到 'beforesend'。如果我只是将文档中的函数添加到我的 js 文件的底部,也会发生同样的情况)

  1. 学生登录他们的日历
  2. 学生加入活动
  3. 警报已触发'newEventData function'
  4. 警报已触发'beforesend'
  5. 警报已触发'we failed'
  6. 抛出错误

    语法错误:无法在 'XMLHttpRequest' 上执行 'setRequestHeader':'$(_csrf.headerName}' 不是有效的 HTTP header 字段名称。

需要协助的地方

其他相关信息

    @Configuration
    @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
    protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    /**
     * Set up url based security
     */
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().and().authorizeRequests().antMatchers("/index.html", "/home.html", 
                "/login.html", "/", "/user","/login")
                .permitAll().anyRequest().authenticated().and()
                .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class).csrf()
                .csrfTokenRepository(csrfTokenRepository()).and().logout();
    }

    @Override
    /**
     * Set up a service to handle authentication requests
     */
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // auth.userDetailsService(userDetailsService).passwordEncoder(new
        // BCryptPasswordEncoder());
        auth.userDetailsService(userDetailsService);
    }

    private CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setHeaderName("X-XSRF-TOKEN");
        return repository;
    }
     }
public class CsrfHeaderFilter extends OncePerRequestFilter {

    /**
     * Overrides SpringSecurity filter
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        System.out.println("In doFilterInternal");
        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        if (csrf != null) {
            System.out.println("csrf is null");
            Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
            String token = csrf.getToken();
            if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                System.out.println("cookie null token not and token doesnt equal cookie");
                cookie = new Cookie("XSRF-TOKEN", token);
                cookie.setPath("/");
                response.addCookie(cookie);
            }
        }
        System.out.println("doing filter chain");
        filterChain.doFilter(request, response);
    }
    }
@RequestMapping(method = RequestMethod.POST, value = "/Student/{studentusername}/universitydays")
public void newEvent(String id, String text, String start, String end, @PathVariable String studentusername) {
    System.out.println(text);
    System.out.println(studentusername);
    System.out.println(id);
    System.out.println(start);
    System.out.println(new Date(Long.parseLong(start)));
    System.out.println(new Date(Long.parseLong(end)));
}

抱歉超长 post,这是我已经研究了一段时间但对 web-app 安全性没有很好理解的东西,所以我正在努力将各个部分组合在一起.任何指导将不胜感激。

干杯, 斯科特

更新

添加了 Paqman 提示的错误参数后,我现在有了以下信息,但仍然不确定如何处理。

SyntaxError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': '${_csrf.headerName}' is not a valid HTTP header field name.

使用我的 chrome 控制台时,这些以文本变量的形式返回。我认为这是不正确的?

我在 index.html header 中的代码(其他页面加载为 'partials')是:

<head>
<title>Study Planner</title>
...css files...
<meta name="_csrf" content="${_csrf.token}" />
<meta name="_csrf_header" content="${_csrf.headerName}" />
</head>

进一步更新

我们通过使用 th:content 而不是 'content' 设法解决了上述文字字符串问题,但这仍然给我们带来了 header/token 未定义的错误。我已经 post 编辑了我们问题的实际解决方案作为答案,但如果有人在 Angular 项目的 html 文件中检索 meta-data 时遇到类似问题,我相信会改变'content' 到 'th:content' 可能会有所帮助。

See this related question

<head>
<title>Study Planner</title>
...css files...
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
</head>

经过一番纠结,我们终于解决了这个问题。尽管我发现的大多数文档都指向存储在会话中的 csrf 令牌,但在我们的应用程序中,我们覆盖了默认的 Spring 安全行为以将其存储在 cookie 中(取自此 SpringBoot Angular Guide)。

 public class CsrfHeaderFilter extends OncePerRequestFilter {

/**
 * Overrides SpringSecurity filter
 */
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
    if (csrf != null) {
        System.out.println("csrf is null");
        Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
        String token = csrf.getToken();
        if (cookie == null || token != null && !token.equals(cookie.getValue())) {
            cookie = new Cookie("XSRF-TOKEN", token);
            cookie.setPath("/");
            response.addCookie(cookie);
        }
    }
    filterChain.doFilter(request, response);
  }

因此,在关注 Spring 文档 here.

时,我们似乎是 'barking up the wrong tree'

意识到这一点后,我们实际上遇到了一些 Django docs 指导我们从 cookie 中检索令牌值。

解决方案

我们将此函数添加到 javascript 中以检索令牌:

function getCookie(name) {
    var cookieValue = null;
    if (document.cookie && document.cookie != '') {
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            var cookie = jQuery.trim(cookies[i]);
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) == (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

将 var 设置为该函数返回的值

 var csrftoken = getCookie('XSRF-TOKEN');

最后对 HEADER 进行了硬编码(不确定我们以后是否会遇到问题),并使用上面为令牌创建的变量。

function newEventData(ev) {
    $.ajax({
        "url" : "/Student/" + loggedinusername
                     + "/universitydays?id=" + ev.id + "&text="
                     + ev.text + "&start="
                     + Date.parse(ev.start_date) + "&end="
                     + Date.parse(ev.end_date),

        "method" : "POST",
        beforeSend : function(xhr) {
              xhr.setRequestHeader("X-XSRF-TOKEN", csrftoken);
        },
        "success" : function() {

        },
        "error" : function(jqXHR, textStatus, errorThrown) {
              alert("error: " + errorThrown);
        } 
 });

}

希望这对遇到类似问题的其他人有所帮助。