10 SpringSecurity-跨域请求伪造(CSRF)的防护

文章配套代码:https://gitee.com/lookoutthebush/spring-security-demo

一、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.启动项目测试

启动项目,登录成功,跳转页面。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七号公园的忧伤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值