csrf

什么是csrf?

CSRF,全称为Cross-Site Request Forgery(跨站请求伪造),是一种Web安全攻击。在CSRF攻击中,攻击者通过诱使受害者在已登录的情况下,访问一个恶意网站或点击恶意链接,来利用受害者的身份在受攻击网站上执行未经授权的操作。

具体来说,CSRF攻击利用了用户的浏览器对于同一站点的认可,即使用户在访问正常站点时是经过授权的(例如登录了一个网站),但用户在访问其他站点时,浏览器仍然会发送相同的认证信息(如Cookie),从而被攻击者利用来执行攻击。攻击者可以以受害者的名义执行例如转账、更改密码等操作,而受害者可能对此毫无察觉。

如何防范CSRF?

为了防范CSRF攻击,常见的措施包括:

  1. CSRF令牌(Token): 服务器生成一个随机的令牌,嵌入到每个表单或者每个请求中,攻击者由于无法获取到这个随机的令牌,无法构造一个有效的请求。

  2. SameSite Cookie属性: 控制浏览器是否在跨站点请求时发送Cookie,可以设置为Strict或者Lax以限制Cookie的发送。

  3. Referer检查: 服务器验证请求来源的Referer头部,但这种方法可被伪造和篡改。

  4. 双重提交Cookie: 将一个随机生成的Cookie和Form表单中的字段进行比较

这篇文章讲解csrf令牌?

在使用Spring Security进行CSRF保护时,确实需要在每个请求中携带CSRF令牌,并在服务器端验证令牌的正确性。以下是关键步骤和策略:

1. 生成和携带CSRF令牌

当用户登录成功后,Spring Security会生成一个CSRF令牌,并将其包含在响应中,通常是作为Cookie的一部分(名为`XSRF-TOKEN`)

- 客户端(通常是浏览器)收到这个令牌后,会将它存储起来,并在后续的请求中自动发送给服务器。

2. 在请求中包含CSRF令牌

在每个涉及到修改数据或执行敏感操作的请求中,客户端需要将CSRF令牌作为参数或者请求头的一部分发送给服务器。

通常情况下,Spring Security会期望CSRF令牌以名为 _csrf 的参数名发送,或者作为名为 X-XSRF-TOKEN 的请求头发送。

3. 服务器端验证CSRF令牌

Spring Security会在后台进行CSRF令牌的验证。验证主要是通过比对请求中的CSRF令牌和服务器端存储的令牌(通常是存储在Cookie中)来实现的。

在Spring Security的配置中,设置了CsrfTokenRequestAttributeHandlerCookieCsrfTokenRepository 来处理和存储CSRF令牌。具体来说:

CsrfTokenRequestAttributeHandler 用于处理请求中的CSRF令牌参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.function.Supplier;
import org.springframework.util.Assert;

public class CsrfTokenRequestAttributeHandler implements CsrfTokenRequestHandler {
private String csrfRequestAttributeName = "_csrf";

public CsrfTokenRequestAttributeHandler() {
}

public final void setCsrfRequestAttributeName(String csrfRequestAttributeName) {
this.csrfRequestAttributeName = csrfRequestAttributeName;
}

public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> deferredCsrfToken) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
Assert.notNull(deferredCsrfToken, "deferredCsrfToken cannot be null");
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = new SupplierCsrfToken(deferredCsrfToken);
request.setAttribute(CsrfToken.class.getName(), csrfToken);
String csrfAttrName = this.csrfRequestAttributeName != null ? this.csrfRequestAttributeName : csrfToken.getParameterName();
request.setAttribute(csrfAttrName, csrfToken);
}

private static final class SupplierCsrfToken implements CsrfToken {
private final Supplier<CsrfToken> csrfTokenSupplier;

private SupplierCsrfToken(Supplier<CsrfToken> csrfTokenSupplier) {
this.csrfTokenSupplier = csrfTokenSupplier;
}

public String getHeaderName() {
return this.getDelegate().getHeaderName();
}

public String getParameterName() {
return this.getDelegate().getParameterName();
}

public String getToken() {
return this.getDelegate().getToken();
}

private CsrfToken getDelegate() {
CsrfToken delegate = (CsrfToken)this.csrfTokenSupplier.get();
if (delegate == null) {
throw new IllegalStateException("csrfTokenSupplier returned null delegate");
} else {
return delegate;
}
}
}
}

CookieCsrfTokenRepository 用于存储和检索CSRF令牌,它默认将CSRF令牌存储在名为 XSRF-TOKEN 的Cookie中,并在需要时使用该Cookie中的令牌值来验证请求中的CSRF令牌

可以查看源码

1
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))

这里是默认的存储和检索csrf令牌的方式,我们进去之后

可以看到返回一个CookieCsrfTokenRepository对象,我们在查看这个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.csrf;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.UUID;
import java.util.function.Consumer;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils;

public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
private static final String CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME = CookieCsrfTokenRepository.class.getName().concat(".REMOVED");
private String parameterName = "_csrf";
private String headerName = "X-XSRF-TOKEN";
private String cookieName = "XSRF-TOKEN";
private boolean cookieHttpOnly = true;
private String cookiePath;
private String cookieDomain;
private Boolean secure;
private int cookieMaxAge = -1;
private Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer = (builder) -> {
};

public CookieCsrfTokenRepository() {
}

public void setCookieCustomizer(Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer) {
Assert.notNull(cookieCustomizer, "cookieCustomizer must not be null");
this.cookieCustomizer = cookieCustomizer;
}

public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
}

public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String tokenValue = token != null ? token.getToken() : "";
ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie.from(this.cookieName, tokenValue).secure(this.secure != null ? this.secure : request.isSecure()).path(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request)).maxAge(token != null ? (long)this.cookieMaxAge : 0L).httpOnly(this.cookieHttpOnly).domain(this.cookieDomain);
this.cookieCustomizer.accept(cookieBuilder);
Cookie cookie = this.mapToCookie(cookieBuilder.build());
response.addCookie(cookie);
if (!StringUtils.hasLength(tokenValue)) {
request.setAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME, Boolean.TRUE);
} else {
request.removeAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME);
}

}

public CsrfToken loadToken(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME))) {
return null;
} else {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return null;
} else {
String token = cookie.getValue();
return !StringUtils.hasLength(token) ? null : new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
}
}

public void setParameterName(String parameterName) {
Assert.notNull(parameterName, "parameterName cannot be null");
this.parameterName = parameterName;
}

public void setHeaderName(String headerName) {
Assert.notNull(headerName, "headerName cannot be null");
this.headerName = headerName;
}

public void setCookieName(String cookieName) {
Assert.notNull(cookieName, "cookieName cannot be null");
this.cookieName = cookieName;
}

/** @deprecated */
@Deprecated(
since = "6.1"
)
public void setCookieHttpOnly(boolean cookieHttpOnly) {
this.cookieHttpOnly = cookieHttpOnly;
}

private String getRequestContext(HttpServletRequest request) {
String contextPath = request.getContextPath();
return contextPath.length() > 0 ? contextPath : "/";
}

public static CookieCsrfTokenRepository withHttpOnlyFalse() {
CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
result.cookieHttpOnly = false;
return result;
}

private String createNewToken() {
return UUID.randomUUID().toString();
}

private Cookie mapToCookie(ResponseCookie responseCookie) {
Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue());
cookie.setSecure(responseCookie.isSecure());
cookie.setPath(responseCookie.getPath());
cookie.setMaxAge((int)responseCookie.getMaxAge().getSeconds());
cookie.setHttpOnly(responseCookie.isHttpOnly());
if (StringUtils.hasLength(responseCookie.getDomain())) {
cookie.setDomain(responseCookie.getDomain());
}

if (StringUtils.hasText(responseCookie.getSameSite())) {
cookie.setAttribute("SameSite", responseCookie.getSameSite());
}

return cookie;
}

public void setCookiePath(String path) {
this.cookiePath = path;
}

public String getCookiePath() {
return this.cookiePath;
}

/** @deprecated */
@Deprecated(
since = "6.1"
)
public void setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain;
}

/** @deprecated */
@Deprecated(
since = "6.1"
)
public void setSecure(Boolean secure) {
this.secure = secure;
}

/** @deprecated */
@Deprecated(
since = "6.1"
)
public void setCookieMaxAge(int cookieMaxAge) {
Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
this.cookieMaxAge = cookieMaxAge;
}
}

可以看到里面主要有三个方法,一个是generateToken,saveToken,loadToken见名思义,一个是生成令牌,一个是保存令牌,最后大概是校验令牌了,csrfFilter 的处理流程很清晰,当一个请求到达时,首先会调用csrfTokenRepository 的loadToken方法加载该会话的CsrfToken值。如果加载不到,则证明请求是首次发起的,应该生成并保存一个新的CsrfToken 值。如果可以加载到CsrfToken 值,那么先排除部分不需要验证CSRF攻击的请求方法(默认忽略了GET、HEAD、TRACE和OPTIONS)

可以看到这里把令牌存到cookie中了

4.实现CSRF令牌验证的步骤

当客户端发送带有CSRF令牌的请求到服务器时,Spring Security会自动进行CSRF令牌验证。你不需要显式地在每个请求处理器中验证CSRF令牌,因为Spring Security框架已经集成了这一功能。

如果CSRF令牌验证失败,Spring Security会阻止请求的执行,并返回相应的错误状态码(通常是403 Forbidden),表明请求被拒绝。

5.自定义处理 CSRF 验证错误

要处理诸如kInvalidCsrfTokenException之类的AccessDeniedException的你可以使用以下配置配置自定义拒绝访问页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling((exceptionHandling) -> exceptionHandling
.accessDeniedPage("/access-denied")
);
return http.build();
}
}

6.禁用 CSRF 保护

默认情况下,CSRF 保护是启用的,这会影响 与后台的集成 和 应用程序的 测试。在禁用 CSRF 保护之前,请考虑这 对你的应用程序是否有意义。

你还可以考虑是否只有某些端点不需要 CSRF 保护,并配置忽略规则,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.ignoringRequestMatchers("/api/*")
);
return http.build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf.disable());
return http.build();
}
}

7.前后端分离同时开启Csrf认证

实现思路,当我们配置csrf时候,登录地址,不需要拦截,登录成功后会得到一个XSRF-TOKEN,前端发送请求之前获取到这个cookie,并将其添加到请求头中,如下图所示

8.额外注意事项

- 确保前端(例如JavaScript应用程序)能够正确地从Cookie中获取CSRF令牌,并将其添加到每个请求的请求头中。

- 在前后端分离的应用中,跨域请求可能需要特别处理,以确保CSRF令牌的正确传递和验证。

总结来说,Spring Security的CSRF保护机制会自动处理CSRF令牌的生成、发送和验证,你只需确保客户端和服务器之间正确地处理CSRF令牌的传递和使用即可。