【问题标题】:With Spring Security 3.2.0.RELEASE, how can I get the CSRF token in a page that is purely HTML with no tag libs使用 Spring Security 3.2.0.RELEASE,如何在没有标签库的纯 HTML 页面中获取 CSRF 令牌
【发布时间】:2014-01-18 16:22:13
【问题描述】:

今天我从带有独立 java config 依赖项的 Spring Security 3.1.4 升级到包含 java config 的新 3.2.0 版本。 CSRF 默认开启,我知道我可以在我的重写配置方法中使用“http.csrf().disable()”禁用它。但是假设我不想禁用它,但我需要登录页面上的 CSRF 令牌,其中没有使用 JSP 标签库或 Spring 标签库。

我的登录页面是纯 HTML,我在使用 Yeoman 生成的 Backbone 应用程序中使用。我将如何以表单或标头的形式包含 HttpSession 中包含的 CSRF 令牌,这样我就不会收到“未找到预期的 CSRF 令牌。您的会话是否已过期?”异常?

【问题讨论】:

  • 在登录页面的响应标头中提供 CSRF 令牌是否有意义?因此,当登录页面被加载时,响应会在标头中包含令牌所需的值。然后我可以在我的表单登录提交中使用该标题。这是一种安全的方法吗?

标签: spring spring-mvc spring-security


【解决方案1】:

我在 Spring Boot 中使用 thymeleaf。我有同样的问题。我通过浏览器诊断出问题查看返回的 html 的来源。它应该是这样的:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<form method="post" action="/login">
<div><label> User Name : <input type="text" name="username" /> </label></div>
<div><label> Password: <input type="password" name="password" /> </label></div>
<input type="hidden" name="_csrf" value=<!--"aaef0ba0-1c75-4434-b6cf-62c975dcc8ba"--> />
<div><input type="submit" value="Sign In" /></div>
</form>
</body>
</html>

如果您看不到此 html 代码。您可能忘记在名称和值之前放置th: 标签。 &lt;input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/&gt;

login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}"> Invalid username and password. </div>
<div th:if="${param.logout}"> You have been logged out. </div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>

【讨论】:

  • 再次。问题要求纯 html - 不是带有 th:value="${_csrf.token}" 的 JSP
【解决方案2】:

注意:我正在使用 CORS 和 AngularJS。

注意²:我发现 Stateless Spring Security Part 1: Stateless CSRF protection 保留 AngularJS 处理 CSRF 的方式会很有趣。

我没有使用基于答案的Spring Security CSRF Filter(尤其是@Rob Winch 的答案),而是使用The Login Page: Angular JS and Spring Security Part II 中描述的方法。

除此之外,我还必须添加 Access-Control-Allow-Headers: ..., X-CSRF-TOKEN(由于 CORS)。

实际上,我发现这种方法比在响应中添加标头更干净。

代码如下:

HttpHeaderFilter.java

@Component("httpHeaderFilter")
public class HttpHeaderFilter extends OncePerRequestFilter {
    @Autowired
    private List<HttpHeaderProvider> providerList;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        providerList.forEach(e -> e.filter(request, response));

        if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
            response.setStatus(HttpStatus.OK.value());
        }
        else {
            filterChain.doFilter(request, response);
        }
    }
}

HttpHeaderProvider.java

public interface HttpHeaderProvider {
    void filter(HttpServletRequest request, HttpServletResponse response);
}

CsrfHttpHeaderProvider.java

@Component
public class CsrfHttpHeaderProvider implements HttpHeaderProvider {
    @Override
    public void filter(HttpServletRequest request, HttpServletResponse response) {
        response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "X-CSRF-TOKEN");
    }
}

CsrfTokenFilter.java

@Component("csrfTokenFilter")
public class CsrfTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        CsrfToken csrf = (CsrfToken)request.getAttribute(CsrfToken.class.getName());

        if (csrf != 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);
    }
}

web.xml

...
<filter>
    <filter-name>httpHeaderFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <async-supported>true</async-supported>
</filter>
<filter-mapping>
    <filter-name>httpHeaderFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
...

security-context.xml

...
<custom-filter ref="csrfTokenFilter" after="CSRF_FILTER"/>
...

app.js

...
.run(['$http', '$cookies', function ($http, $cookies) {
    $http.defaults.transformResponse.unshift(function (data, headers) {
        var csrfToken = $cookies['XSRF-TOKEN'];

        if (!!csrfToken) {
            $http.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;
        }

        return data;
    });
}]);

【讨论】:

  • 您不应该使用 cookie 来存储令牌。它可以被黑客入侵。
  • 正如 Alessandro 之前所说,不要通过 cookie 发送令牌。 CSRF 的重点是攻击者让受害者发送一个请求,该请求会自动使用受害者的 cookie 和会话 ID。令牌是一个秘密,不应自动发送,而是作为隐藏的表单字段或标题发送。将其放入 cookie 中完全违背了目的。
【解决方案3】:

虽然@rob-winch 是对的,但我建议从会话中获取令牌。如果 Spring-Security 使用CsrfAuthenticationStrategySessionManagementFilter 中生成新令牌,它将把它设置为 Session 但不在 Request 上。所以你最终可能会得到错误的 csrf 令牌。

public static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
CsrfToken sessionToken = (CsrfToken) request.getSession().getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME);

【讨论】:

  • 使用 HttpSessionCsrfTokenRepository#loadToken() 可能会更干净
【解决方案4】:

您可以使用名为 _csrf 的请求属性获取 CSRF,如 the reference 中所述。要将 CSRF 添加到 HTML 页面,您需要使用 JavaScript 来获取需要包含在请求中的令牌。

将令牌作为标头返回比在正文中作为 JSON 返回更安全,因为正文中的 JSON 可以由外部域获取。例如,您的 JavaScript 可以请求由以下内容处理的 URL:

CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
// Spring Security will allow the Token to be included in this header name
response.setHeader("X-CSRF-HEADER", token.getHeaderName());
// Spring Security will allow the token to be included in this parameter name
response.setHeader("X-CSRF-PARAM", token.getParameterName());
// this is the value of the token to be included as either a header or an HTTP parameter
response.setHeader("X-CSRF-TOKEN", token.getToken());

然后,您的 JavaScript 将从响应标头中获取标头名称或参数名称和令牌,并将其添加到登录请求中。

【讨论】:

  • 该博客的代码似乎比我的答案多很多。您有什么理由喜欢您的解决方案?
  • 我还添加了一个 JIRA 来改进这方面的文档。见jira.springsource.org/browse/SEC-2460
  • 我正在使用带有 SpringSecurity3.2 的 AngularJS,并在我的 index.html 中使用了元标记,正如您所指出的。我会收到“未找到预期的 CSRF 令牌。您的会话是否已过期?”,如果 Spring 真的在生成 CSRF 令牌,有没有办法可以在 ChromeTools/Firebug 中验证?
  • 确保您的标记在元标记中包含令牌。您还应该验证您的 HttpSession 在创建令牌和使用它的请求之间没有变化。
  • 我在头部使用 &lt;meta name="_csrf" content="${_csrf.token}"&gt;&lt;meta name="_csrf_header" content="${_csrf.headerName}"&gt;,但我没有看到占位符被 csrf 令牌替换。另外,如何确保 http 会话在登录前后不会改变。对我来说,登录后似乎创建/更改了 http 会话。
猜你喜欢
  • 2014-02-07
  • 1970-01-01
  • 2014-04-26
  • 2014-03-12
  • 2014-10-26
  • 2017-09-23
  • 2022-07-29
  • 2020-08-08
  • 2019-12-19
相关资源
最近更新 更多