一、简介
CSRF(Cross-Site Request Forgery 跨站请求伪造),也可称为一键式攻击(one-click-attack)通常缩写为 CSRF 或者 XSRF。
CSRF 攻击是一种挟持用户在当前已登录的浏览器上发送恶意请求的攻击方法。相对于XSS利用用户对指定网站的信任,CSRF则是利用网站对用户网页浏览器的信任。简单来说,CSRF是致击者通过一些技术手段欺骗用户的浏览器,去访问一个用户曾经认证过的网站并执行恶意请求,例如发送邮件、发消息、甚至财产操作(如转账和购买商品) 。由于客户端浏览器)已经在该网站上认证过,所以该网站会认为是真正用户在操作而执行请求(实际上这个并非用户的本意)。
举个例子:
假设张三现在登录了某银行的网站准备完成一项转账操作,转账的链接如下:
https: //bank .xxx .com/withdraw?account=张三&amount=1000&for=李四 可以看到,这个链接是想从张三这个账户下转账 1000 元到李四 账户下,假设张三 没有注销登录该银行的网站,就在同一个浏览器新的选项卡中打开了一个危险网站,这个危险网站中有一幅图片很很有诱惑力,你立马点开了,代码如下:
网<img src="https ://bank.xxx.com/withdraw?account=张三&amount=10008for=黑客">
一旦用户打开了这个网站,这个图片链接中的请求就会自动发送出去。由于是同一个浏览器并且用户尚未注销登录,所以该请求会自动携带上对应的有效的 Cookie 信息,进而完成一次转账操作。这就是跨站请求伪造。
二、csrf 防御
CSRF攻击的根源在于浏览器默认的身份验证机制(自动携带当前网站的Cookie信息),这种机制虽然可以保证请求时来自用户的某个浏览器,但是无法确保这请求是用户授权发送。攻击者和用户发送的请求一模一样,这意味着我们没有办法去直接拒绝这里的某一个请求。如果能在合法清求中额外携带一个攻击者无法获取的参数,就可以成功区分出两种不同的请求进而直接拒绝掉恶意请求。在 SpringSecurity 中就提供了这种机制来防御 CSRF 攻击,这种机制我们称之为 令牌同步模式。
2.1 令牌同步模式
这是目前主流的CSRF 攻击防御方案。具体的操作方式就是在每一个 HTTP 请求中,除了默认自动携带的 Cookie 参数之外,再提供一个安全的、随机生成的宇符串,我们称之为 CSRF令牌。这个 CSRF 令牌由服务端生成,生成后在 HtpSession 中保存一份。当前端请求到达后,将请求携带的 CSRF 令牌信息和服务端中保存的令牌进行对比,如果两者不相等,则拒绝掉该 HITTP 请求。
注意:考虑到会有一些外部站点链接到我们的网站,所以我们要求请求是幂等的,这样对子HEAD、OPTIONS、TRACE 等方法就没有必要使用 CSRF 令牌了,强行使用可能会导致令牌泄露!
2.2 开启csrf 防御
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin() // 开启表单认证
.and()
.csrf();// 开启csrf 攻击
}
}
2.2.1 当我们打开csrf 防御后效果
初看登录页面倒是没有什么不同,我们需要右键查看源代码你就会发现不同点了;表单中多了<input name="_csrf" type="hidden" value="b634774a-6295-4d1c-9d32-9c639a25cb76" />
2.2.2 自定义表单
我们看到,在我们自定义的表单中,没有加_csrf标签,当我们访问 index页面时候,我们查看源码,默认给我们加上了 Input 这个隐藏标签
<h2> 欢迎进入首页 </h2>
<form role="form" class="form-horizontal" th:action="@{/hello}" method="post">
<div class="form-group">
<label class="control-label col-sm-2">输入框</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="loginId" placeholder="测试">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">提交</button>
</div>
</div>
</form>
三、传统web开发使用csrf
开启CSRF防御后会自动在提交的表单中加入如下代码,如果不能自动加入,需要在开启之后手动加入如下代码,并随着请求提交。获取服务端令牌方式如下:
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
3.1 后端配置
@Controller
public class HelloController {
@RequestMapping("hello")
@ResponseBody
public String hello() {
System.out.println("hello===========>");
return "hello ";
}
@RequestMapping("index.html")
public String index() {
return "index ";
}
}
3.2 前端页面
<form role="form" class="form-horizontal" th:action="@{/hello}" method="post">
<div class="form-group">
<label class="control-label col-sm-2">输入框</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="loginId" placeholder="测试">
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">提交</button>
</div>
</div>
</form>
3.3 查看效果
我们看到,当我们手动写了一个hidden后,页面又给我们自动添加了一个,默认会给我们自动生成一个,如果不能自动生成,那么就需要手动写入_csrf 了;
3.4 源码查看
org.springframework.security.config.annotation.web.configurers.CsrfConfigurer#configure 入口启动类
org.springframework.security.web.csrf.CsrfFilter#doFilterInternal 实际真正的过滤规则
org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository#loadToken 至于为什么是这么,我们看默认的配置
从当前session中获取一个属性为 HttpSessionCsrfTokenRepository.CSRF_TOKEN
当在session中获取不到的时候,会重新生成一个token并且放到session以及request请求中
进行 Token对比,先从header中获取X-CSRF-TOKEN,如果请求头没有的话,就从请求参数中获取一个叫_csrf 参数,然后做对比,如果对比失败,则抛出异常
四、前后端分离使用csrf
前后端分离开发时,只需要将生成 csrf 放入到cookie 中,并在请求时获取 cookie 中令牌信息进行提交即可。
4.1 修改 CSRF 存入 Cookie
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());//将令牌保存到 cookie 允许前端在cookie中获取
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()// 所有请求都需要认证
.and()
.formLogin()// 表单配置
.and().exceptionHandling()
.authenticationEntryPoint(((request, response, authException) -> {
// 判断是否有登录
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
WebRespUtils.writeJson(response,"请认证后再来请求接口");
} else {
WebRespUtils.writeJson(response,authException.getLocalizedMessage());
}
}))
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
//替换掉UsernamePasswordAuthenticationFilter
http.addFilterAt(loginVerifyImgFilter(), UsernamePasswordAuthenticationFilter.class);
4.2 登录后查看cookie效果
第一请求的时候,失败了,因为没有携带cookie参数;
这个时候我们将该参数放到请求头header中再试试,注意请求header的值为:X-XSRF-TOKEN
4.2.1 这一部分对应源码
4.3 这个时候我不带token访问
我们直接访问 hello接口 ,我们看到直接报403 了,因为没有携带这个 csrf token参数
带上请求参数,不用header尝试,可以看到也可以成功
4.4 登录过滤器代码
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
Map<String,String> userMap = null;
try {
// 用户信息
userMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (Exception e) {
e.printStackTrace();
}
// 获取图形验证码
String username = userMap.get(getUsernameParameter());
String password = userMap.get(getPasswordParameter());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
return super.attemptAuthentication(request,response);
}
}