csrf概念
英文全称叫做: cross-site request forgery,翻译过来叫做跨站请求伪造。spring security默认情况下是开启了csrf保护的。所谓的CSRF一般会在用户做了某个动作之后附加了一些额外动作。
csrf工作机制
往往是利用用户在某个系统中已经登录过,其具有一些服务器操作资源的权限,攻击者利用伪造的链接或其它骗用户完成某个操作,而这个操作会偷偷和该用户可操作的系统交互。
一个简单的示例图
如何解决
根本问题在于,我们必须要确定这个行为是不是由本身客户端发出的,而并非其它站点间接请求的,所以解决方案是建立一个信任机制,服务端必须和客户端有一个数据项来确认合法性。
spring security的解决方案
spring security通过过滤器CsrfFilter来解决这个问题 。
1 对于请求先加载得到CsrfToken对象
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
实现上常见的方式两个
- 基于Cookie
- 基于Session
虽然上图写有4个,但是第三个是一个委托模式,第四个是用于测试的。
2 看token的缺失性
boolean missingToken = (csrfToken == null);
如果csrfToken对象是null,缺失性为true。
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
如果缺失,这个token对象重新复制,生成一个token。并且将其保存。
3 保存数据
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
然后再请求中存储了两个数据。
4 匹配
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
+ this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
return;
}
如果请求能够匹配到才能走里面的过滤器。
5 拿真正的token
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
this.logger.debug(
LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
filterChain.doFilter(request, response);
- 从头部信息中获取实际token
- 如果没有,再从参数中拿token
- 如果实际的token和用户当前带过来的token不同,访问拒绝,给出异常
- 如果比较相等,继续下一个过滤器
CsrfTokenRepository接口
public interface CsrfTokenRepository {
/**
* Generates a {@link CsrfToken}
* @param request the {@link HttpServletRequest} to use
* @return the {@link CsrfToken} that was generated. Cannot be null.
*/
CsrfToken generateToken(HttpServletRequest request);
/**
* Saves the {@link CsrfToken} using the {@link HttpServletRequest} and
* {@link HttpServletResponse}. If the {@link CsrfToken} is null, it is the same as
* deleting it.
* @param token the {@link CsrfToken} to save or null to delete
* @param request the {@link HttpServletRequest} to use
* @param response the {@link HttpServletResponse} to use
*/
void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
/**
* Loads the expected {@link CsrfToken} from the {@link HttpServletRequest}
* @param request the {@link HttpServletRequest} to use
* @return the {@link CsrfToken} or null if none exists
*/
CsrfToken loadToken(HttpServletRequest request);
}
这个接口做三个简答的事情
- 生成token
- 存储token
- 加载token
写读(制造)
它的作用就是生成服务端的token,并保存,将来再取出来和客户端发过来的实际token比较。
实践
我们来定义一个简单的端点
package com.qiudaozhang.springsecurity.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("hello")
public String getHello () {
return "get hello";
}
@PostMapping("hello")
public String postHello () {
return "post";
}
}
基础配置
package com.qiudaozhang.springsecurity.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.permitAll();
}
}
现在get请求
curl -XGET http://localhost:8080/hello
post请求
curl -XPOST http://localhost:8080/hello
发现失败了。
404,被禁止,没有这个权限。
解决禁止问题
当前我们为了获取到这个token,我们想办法记录一下他,由于这个是CsrfFilter生成的,所以我们可以设计一个过滤器在它支持将它查出来。
通过这个类我们知道,它存储头部用的是X-XSRF-TOKEN
,在参数中用的_csrf
,所以我们的过滤器只需要在一次访问后去拿去即可。
自定义过滤器
获取token信息
package com.qiudaozhang.springsecurity.filter;
import org.springframework.security.web.csrf.CsrfToken;
import javax.servlet.*;
import java.io.IOException;
public class CsrfTokenLoggerFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Object csrf = request.getAttribute("_csrf");
CsrfToken csrfToken = (CsrfToken) csrf;
System.out.println("token :" + csrfToken.getToken());
chain.doFilter(request,response);
}
}
其它步骤
【拿 SessionID】
首先我们发起一次get请求拿到 SessionID
【拿token】
token :9d6c99c7-6703-490e-9e5b-40edb32351dd
【重新post请求】
curl -X POST http://localhost:8080/hello -H "Cookie: JSESSIONID=B6E44E07AFE17C5F44D7EB871828F18F" -H "X-CSRF-TOKEN:9d6c99c7-6703-490e-9e5b-40edb32351dd"
这就成功了。
表单提交保护
too 未完待续
源码
https://github.com/qiudaozhang/spring-security-csrf01