使用 Spring Security 3.2.0.RELEASE,如何在没有标签库的纯 HTML 页面中获取 CSRF 令牌
Posted
技术标签:
【中文标题】使用 Spring Security 3.2.0.RELEASE,如何在没有标签库的纯 HTML 页面中获取 CSRF 令牌【英文标题】: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 【发布时间】: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 令牌是否有意义?因此,当登录页面被加载时,响应会在标头中包含令牌所需的值。然后我可以在我的表单登录提交中使用该标题。这是一种安全的方法吗? 【参考方案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:
标签。 <input type="hidden" th:name="$_csrf.parameterName" th:value="$_csrf.token"/>
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 使用CsrfAuthenticationStrategy
在SessionManagementFilter
中生成新令牌,它将把它设置为 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 在创建令牌和使用它的请求之间没有变化。 我在头部使用<meta name="_csrf" content="$_csrf.token">
和 <meta name="_csrf_header" content="$_csrf.headerName">
,但我没有看到占位符被 csrf 令牌替换。另外,如何确保 http 会话在登录前后不会改变。对我来说,登录后似乎创建/更改了 http 会话。以上是关于使用 Spring Security 3.2.0.RELEASE,如何在没有标签库的纯 HTML 页面中获取 CSRF 令牌的主要内容,如果未能解决你的问题,请参考以下文章
在使用 Spring Security 3.2.0 保护应用程序并提供 javascript 文件时,如何设置内容类型标头?
使用 Spring Security 3.2.0.RELEASE,如何在没有标签库的纯 HTML 页面中获取 CSRF 令牌
spring security 3.2.0 csrf 令牌在 freemarker 模板中不起作用
Spring security 3.2.0 RC1 csrf with multipart/form-data