下面我们以一个具体的例子来说明这种常见的攻击模式
1.1 假定某个银行的网站提供让当前登录用户给其他账号转账的功能,转账请求的格式如下
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
1.2 现在有一个用户登录到了这个银行的网站并且进行了认证,在未logout的情况下访问了一个恶意网站,恶意网站包含如下html代码段
这时如果用户点击了【领取奖品】按钮,你将给那个恶意攻击者的账户转入100元,这是因为虽然恶意网站不能获取到用户cookie信息,但是当点击【领取奖品】按钮访问银行网站时,cookie信息还是会一起发送。更糟糕的是,借助于javascript,上面的过程可以自动执行,不需要等待用户点击【领取奖品】按钮,只要你打开了这个页面,你的钱就被偷走了,这也是我们常见的钓鱼网站的做法。
像这样虽然用户访问的是另一个网站,但是这个网站却伪装成当前认证用户访问用户正在访问的网站以实现攻击的方式称为CSRF。
2.1 从上面的例子可以看出,无论转账请求是从银行自己的网站发出,还是从恶意网站发出,对于银行的服务端来说内容是一样的,所以单纯从服务端来讲我们没法过滤掉那些恶意的请求。如果我们能采取一种措施让银行的正常页面发请求时给服务器提供一个凭证,并且这个凭证是恶意网站所不能提供的,这样服务器端就可以很容器拒绝掉那些非法的请求了。
同步token就是这样的一种方式,他要求在客户端发起请求时,除了cookie信息外,还需要提供一个随机的token值作为参数。当服务器收到一个请求后,会先解析出这个token值,再和期望的值进行比较,如果不匹配则拒绝提供服务。
2.2 在实际项目中,我们可以放宽上面的规则,只要求那些会修改信息的请求才提供token值,因为根据同源策略,那些恶意网站是不能获取到我们正常请求的响应结果的。追加完token后,我们的请求示例如下:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876&_csrf=
这样,因为恶意网站不能狗提供_csrf对应的随机值,伪造的请求将不会被服务器接受。
动词 | 作用 | 类比数据库操作 |
---|---|---|
GET | 从服务器获取信息 | select |
POST | 在服务器上新创建一个资源 | insert |
PUT | 更新服务器上的一个资源,本次请求包含完整的信息 | update |
PATCH | 更新服务器上的一个资源,包含部分信息 | update |
DELETE | 删除服务器上的一个资源 | delete |
HEAD | 向服务器索要与GET请求相一致的响应,只不过只有头部信息,响应体将不会被返回。 | 无 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断 | 无 |
OPTIONS | 返回服务器针对特定资源所支持的HTTP请求方法 | 无 |
确保对信息进行修改的请求其动词一定是post、put、patch、delete中的一种。
默认情况下,Java Configuration会启用CSRF保护。如果要禁用CSRF,可以在下面看到相应的Java配置。有关如何配置CSRF保护的其他自定义,请参阅csrf()的Javadoc。
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
}
一些框架通过使用户的会话无效来处理无效的CSRF令牌,但这会导致其自身的问题。相反,默认情况下,Spring Security的CSRF保护将产生HTTP 403访问被拒绝。这可以通过配置AccessDeniedHandler以进行InvalidCsrfTokenException不同的处理来自定义。自定义AccessDeniedHandler
我们要确保在所有执行post、put、patch、delete的请求中包含CSRF token值,最直接的方式是使jstl表达式从request中获取到_csrf对应的值,如下
另外spring 也为我们提供了两个方便的jsptag。具体参考例子
在某些场合下,我们可能会需要将csrf token值存储在cookie中,此时可以用CookieCsrfTokenRepository来实现这个功能,默认情况下,写入到cookie中的key是XSRF-TOKEN,读取时从request header的X-XSRF-TOKEN中或者parameter的_csrf中读取。可以用如下代码段配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(new CookieCsrfTokenRepository());
}
}
默认情况下设置到cookie中的信息不能够通过js读取,如果需要js访问的话需要明确设定
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
默认情况下,csrf token存储在httpsession中,当session过期时AccessDeniedHandler会收到一个InvalidCsrfTokenException,如果我们使用的是spring security默认的AccessDeniedHandler,客户端将会收到一个简单的403错误(因为 CSRF_FILTER在认证filter的后面),对于这个问题,我们可以采用下面几种方式处理
如果在登录页面也启用csrf token保护,就需要在登录前生成CsrfToken时创建HttpSession,这时就需要考虑如果用户在登录页面长时间停留,会引起session过期问题,当登录时直接返回403(没权限登录–),现在通用的解决方法是采用JavaScript在点击登录时,先获取token值,接着在提交登录请求,这样session在登录时才创建,用户就可以在登录界面停留任意时间了,利用CsrfTokenArgumentResolver我们很容易实现这样的功能
默认情况下,启用csrf token后,LogoutFilter只接收Post请求,并且logout时还需要提供csrf token值
@SuppressWarnings("unchecked")
private RequestMatcher getLogoutRequestMatcher(H http) {
if (logoutRequestMatcher != null) {
return logoutRequestMatcher;
}
if (http.getConfigurer(CsrfConfigurer.class) != null) {
this.logoutRequestMatcher = new AntPathRequestMatcher(this.logoutUrl, "POST");
}
else {
this.logoutRequestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(this.logoutUrl, "GET"),
new AntPathRequestMatcher(this.logoutUrl, "POST"),
new AntPathRequestMatcher(this.logoutUrl, "PUT"),
new AntPathRequestMatcher(this.logoutUrl, "DELETE")
);
}
return this.logoutRequestMatcher;
}
如果Logout操作安全性没有那么高,实现时不想这么复杂,可以通过下面代码段配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
}
}
这时可以用任意的HTTP method执行logout操作
可通过以下两种方式解决
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}