SpringSecurity跨域请求伪造(CSRF)的防护实现
作者:七号公园的忧伤
一、CSRF
CSRF的全称是(Cross Site Request Forgery),可译为跨域请求伪造,是一种利用用户带登录 态的cookie进行安全操作的攻击方式。CSRF实际上并不难防,但常常被系统开发者忽略,从而埋下巨 大的安全隐患。
二、攻击过程
举个例子,假设你登录了邮箱,正常情况下可以通过某个链接http:xx.mail.com/send可以发送邮件。此时你又访问了别的网站,网站中有黄色广告,点击后广告会请求http:xx.mail.com/send。此时相当于在盗版网站中调用了发送邮件的链接,访问时会使用你邮箱网站的cookie信息。虽然盗版网站会提示跨域,但服务端任然进行了相应处理。
三、防御手段
在任何情况下,都应当尽可能地避免以GET方式提供涉及数据修改的API。并不是说其他请求方式可以避免CSRF,只是GET请求更容易被攻击。
在此基础上,防御 CSRF攻击的方式主要有以下两种。
1.HTTP Referer
Http referer是由浏览器添加的一个请求头字段,用于标识请求来源,浏览器端无法轻易篡改该值。
比如攻击者在第三方页面构造了POST请求,htttp referer不是我们网站的地址(有的老版IE浏览器可以修改该值,如果用户的浏览器比较新,就能避免这个问题),当服务端收到请求,发现请求来自其他站点,就能拒绝该请求。
这种方式简单便捷,但不是完全可靠,比如老的浏览器就能修改该值。用户在浏览器设置了不被跟踪,就不会有该字段,服务端加了校验后就会拦截掉用户的正常请求。
2.CsrfToken认证
CSRF是利用用户的登录态进行攻击的,而用户的登录态记录在cookie中。其实攻击者并不知道用 户的cookie存放了哪些数据,于是想方设法让用户自身发起请求,这样浏览器便会自行将cookie传送到 服务器完成身份校验。
CsrfToken 的防范思路是,添加一些并不存放于 cookie 的验证值,并在每个请求中都进行校验, 便可以阻止CSRF攻击。
具体做法是在用户登录时,由系统发放一个CsrfToken值,用户携带该CsrfToken值与用户名、密码 等参数完成登录。服务端记录该会话的CsrfToken值,之后在用户的任何请求中,都必须带上该 CsrfToken值,并由系统进行校验。
该方案需要前端配合,包括存储CsrfToken的值,在每次的请求中,不管是form表单还是ajax,都需要携带该token。虽然比HTTP Referer安全很多,但也有弊端,如果在已有系统进行改造,就需要修改每一个请求,所以建议在系统开发之初就考虑防御CSRF攻击。
三、使用SpringSecurity防御CSRF
csrf攻击完全是基于浏览器的,如果前端没有浏览器,也就不会有CSRF攻击了,所以我们需要关闭SpringSecurity自动配置的csrf。
1.SpringSecurity防御CSRF过程
CsrfFilter:
SpringSecurity通过注册一个CsrfFilter来专门处理CSRF攻击。
CsrfToken:
用该接口来定义csrftoekn所需的一些必要方法。
public interface CsrfToken extends Serializable { //从哪个头字段获取token值 String getHeaderName(); //从哪个参数获取token值 String getParameterName(); String getToken(); }
CsrfTokenRepository
定义了如何生成,保存、以及加载token.
public interface CsrfTokenRepository { CsrfToken generateToken(HttpServletRequest request); void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response); CsrfToken loadToken(HttpServletRequest request); }
HttpSessionCsrfTokenRepository
默认情况下,SpringSecurity使用的CsrfTokenRepository的实现类是HttpSessionCsrfTokenRepository
public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository { private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"; private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName() .concat(".CSRF_TOKEN"); private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; private String headerName = DEFAULT_CSRF_HEADER_NAME; private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME; @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (token == null) { HttpSession session = request.getSession(false); if (session != null) { session.removeAttribute(this.sessionAttributeName); } } else { HttpSession session = request.getSession(); session.setAttribute(this.sessionAttributeName, token); } } @Override public CsrfToken loadToken(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return null; } return (CsrfToken) session.getAttribute(this.sessionAttributeName); } @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } public void setParameterName(String parameterName) { Assert.hasLength(parameterName, "parameterName cannot be null or empty"); this.parameterName = parameterName; } public void setHeaderName(String headerName) { Assert.hasLength(headerName, "headerName cannot be null or empty"); this.headerName = headerName; } public void setSessionAttributeName(String sessionAttributeName) { Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty"); this.sessionAttributeName = sessionAttributeName; } private String createNewToken() { return UUID.randomUUID().toString(); } }
HttpSessionCsrfTokenRepository将CsrfToken值存储在HttpSession中,并指定前端把CsrfToken 值放在名为“_csrf”的请求参数或名为“X-CSRF-TOKEN”的请求头字段里(可以调用相应的设置方法来重新设定)。校验时,通过对比HttpSession内存储的CsrfToken值与前端携带的CsrfToken值是否一致,便能断定本次请求是否为CSRF攻击。
前端使用Token的时候,必须使用从服务端渲染的方式,比如jsp页面:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
CookieCsrfTokenRepository
Spring Security还提供了另一种方式,即CookieCsrfTokenRepository。之前是服务端将token存储在了session中。这个是将token存储在浏览器的cookie中,这样可以减少服务端的内存消耗,而且前端可以使用js读取(需要设置该cookie的httpOnly属性为false),更加灵活。
有人可能会有疑问,放在cookie中,不是又可以被攻击了吗?其实不是的。
cookie只有在同域的情况下才能被js获取。正常情况下,服务端从cookie中获取token,前端使用js从cookie中获取token,2者进行校验。攻击者只能在第三方页面伪造请求的时候,利用请求携带cookie,这个时候服务端能拿从携带的cookie中拿到token,但是前端并没有使用js将用于校验的token传给服务端(攻击者没法获取cookie),所以校验没法通过。
CsrfFilter
现在我们重新来看这个类的主要逻辑:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); CsrfToken csrfToken = this.tokenRepository.loadToken(request); boolean missingToken = (csrfToken == null); if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); 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; } String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!equalsConstantTime(csrfToken.getToken(), 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); }
这段代码的意思就是, 从你指定或者默认的的CsrfTokenRepository中获取token,其实就是获取的服务端存储的token(session中或者cookie中),如果没有,那么就生成并且保存token,然后获取前端传过来的token,然后进行对比。
2.SpringSecurity配置CSRF
1.我们使用cookie的方式存储token.
2.添加AccessDeniedHandler
用来在token请求不通过的时候,返回数据。
@Component @Slf4j public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(ResultVO.error(10000, "禁止访问"))); } }
3. 前端修改
生成的token:
function getCookie(name){ var strcookie = document.cookie;//获取cookie字符串 var arrcookie = strcookie.split("; ");//分割 //遍历匹配 for ( var i = 0; i < arrcookie.length; i++) { var arr = arrcookie[i].split("="); if (arr[0] === name){ return arr[1]; } } return ""; }
3.启动项目测试
启动项目,登录成功,跳转页面。
文章配套代码:https://gitee.com/lookoutthebush/spring-security-demo
到此这篇关于SpringSecurity跨域请求伪造(CSRF)的防护实现的文章就介绍到这了,更多相关SpringSecurity CSRF防护内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!