本文转载于SegmentFault社区
作者:Mshu
CSRF 全称:Cross Site Request Forgery,译:跨站请求伪造
场景
点击一个链接之后发现:账号被盗,钱被转走,或者莫名发表某些评论等一切自己不知情的操作。
CSRF是什么
csrf 是一个可以发送http请求的脚本。可以伪装受害者向网站发送请求,达到修改网站数据的目的。
原理
当你在浏览器上登录某网站后,cookie会保存登录的信息,这样在继续访问的时候不用每次都登录了,这个大家都知道。而CSRF就利用这个登陆态去发送恶意请求给后端。
为什么脚本可以获得目标网站的cookie呢?
只要是请求目标网站,浏览器会自动带上该网站域名下面的cookie,看下面的脚本,可以证明恶意脚本可以获得CSDN网站的登录信息。
前提是你已经在浏览器上登录了CSND网站。
< html> < head> < metacharset= "utf-8"/> < title> csrf demo title> head> < body> 您在CSDN上的粉丝数: < spanid= "fans_num"> span> 关注数: < spanid= "follow_num"> span> < > fetch( 'https://me.csdn.net/api/relation/get', { credentials: 'include'}).then( res=> res.json) .then(res=> { document.getElementById( 'fans_num').innerText = res.data.fans_num; document.getElementById( 'follow_num').innerText = res.data.follow_num; }) > body> html>
保证CSDN的登录状态,用浏览器打开这个html文件,可以看到这个脚本已经获得了我在csdn 上的用户信息。以及寒酸的粉丝数量!
F12打开选择应用程序一栏左边Cookie 还有来自csdn网站关于当前用户的一些信息。
这个脚本让每个不同的登录用户打开,都会根据当前用户来展示关注数和粉丝数,这就足以说明可以获得目标网站的当前用户的信息,并能够代表用户发送请求。
这只是个无害的get请求,如果是post请求呢?
CSRF攻击
知道了原理,攻击就变得好理解了,接着上面的例子,
我把请求地址改成评论本篇文章的url,参数为 “这篇文章写得6”,在没有CSRF防御的情况下,我发表一个评论如:脱单秘笈:,后面附上这个脚本的链接,只要有用户点了链接,就会以他的名义给本篇文章发评论“这篇文章写得6”。
CSDN 肯定是做了防御了哈,我就不白费力气了。
CSRF防御
三种防御方式:
1. SameSit
禁止第三方网站使用本站Cookie。
这是后端在设置Cookie时候给SameSite的值设置为Strict或者Lax。
当设置Strict的时候代表第三方网站所有请求都不能使用本站的Cookie。
当设置Lax的时候代表只允许第三方网站的GET表单、标签和标签携带Cookie。
当设置None的时候代表和没设一样。
@BeanpublicCookieSerializer httpSessionIdResolver{ DefaultCookieSerializer cookieSerializer = newDefaultCookieSerializer; cookieSerializer.setCookieName( "JESSIONID"); cookieSerializer.setUseHttpOnlyCookie( true); cookieSerializer.setSameSite( "Lax"); cookieSerializer.setUseSecureCookie( true); returncookieSerializer; }
缺点:
目前只有chrome浏览器支持........
2. referer
referer代表着请求的来源,不可以伪造。
后端写个过滤器检查请求的headers中的referer,检验是不是本网站的请求。
题外话:
referer和origin的区别,只有post请求会携带origin请求头,而referer不论何种情况下都带。
referer正确的拼写 应该是 referrer,HTTP的标准制定者们将错就错,不打算改了
缺点:
浏览器可以关闭referer..........
3. token
最普遍的一种防御方法,后端生成一个token放在session中并发给前端,前端发送请求时携带这个token,后端通过校验这个token和session中的token是否一致判断是否是本网站的请求。
具体实现:
用户登录输入账号密码,请求登录接口,后端在用户登录信息正确的情况下将token放到session中,并返回token给前端,前端把token 存放在localstory中,之后再发送请求都会将token放到header中。
后端写个过滤器,拦截POST请求,注意忽略掉不需要token的请求,比如登录接口,获取token的接口,免得还没有获取token就检验token。
校验原则: session中的token和前端header中的token一致的post ,放行。
/*** @authormashu * Date 2020/6/22 9:37*/@Slf4j @Component@WebFilter(urlPatterns = "/*", filterName = "verificationTokenFilter", deion = "用于校验token") publicclassVerificationTokenFilterimplementsFilter{
List
@Overridepublicvoidinit(FilterConfig filterConfig)throwsServletException { }
@OverridepublicvoiddoFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)throwsIOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;//忽略不需要token的请求String serviceUrl = httpServletRequest.getServletPath;for( finalString ignorePath : ignorePathList) { if(serviceUrl.contains(ignorePath)) { filterChain.doFilter(servletRequest, servletResponse);return; }}String method = httpServletRequest.getMethod;if( "POST".equals(method)) { String tokenSession = (String)httpServletRequest.getSession.getAttribute( "token"); String token = httpServletRequest.getHeader( "token"); if( null!= token && null!= tokenSession && tokenSession.equals(token)) { filterChain.doFilter(servletRequest, servletResponse);return; } else{ log.error( "验证token失败!"+ tokenSession + "!="+ token); httpServletResponse.sendError( 403); return; }}filterChain.doFilter(servletRequest, servletResponse);}
@Overridepublicvoiddestroy{
}}