查看csrfFilter源码,会先去HttpSessionCsrfTokenRepository.loadToken加载CsrfToken ,其实就是从session中获取。
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (CsrfToken) session.getAttribute(this.sessionAttributeName);
}
如果不存在,会创建一个CsrfToken 并放入session
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);
}
}
之后会从request中取token,与从session中取出的token对比,所以这里开启csrf认证后,这四种请求是不验证csrf的”GET”, “HEAD”, “TRACE”, “OPTIONS”,常用的POST请求,每次都要么在请求头上加上X-CSRF-TOKEN:csrf值,或者在POST中加入参数_csrf:csrf值,这样才能取出request中的csrf和session中的对比。
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
如果request中取出的csrf和session中取出的不相等,会进入accessDeniedHandler.handle,这个accessDeniedHandler是AccessDeniedHandlerImpl,里面方法如下
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
if(!response.isCommitted()) {
if(this.errorPage != null) {
request.setAttribute("SPRING_SECURITY_403_EXCEPTION", accessDeniedException);
response.setStatus(403);
RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPage);
dispatcher.forward(request, response);
} else {
response.sendError(403, accessDeniedException.getMessage());
}
}
如果设置了errorPage会服务器内部转发到该路径,之后还是会经过security的各个filter进行登陆操作,最终登陆成功。这不是我要的,,所以需要自定义一个accessDeniedHandler。代码如下,如果不是登陆请求的 csrf不匹配,都退出当前用户,登陆用户不做csrf校验。
@Component
public class CsrfAccessDeniedHandler implements AccessDeniedHandler {
private SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
@Autowired
private AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
logoutHandler.logout(request, response, auth);
ajaxLogoutSuccessHandler.onLogoutSuccess(request, response, auth);
}
然后在securityConfig里配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 允许iframe
http.headers().frameOptions().sameOrigin();
//登陆页面不做csrf校验
http.csrf().ignoringAntMatchers("/login");
//异常处理
http.exceptionHandling().
accessDeniedHandler(csrfAccessDeniedHandler)
}
这里有一篇讨论csrf的文章https://segmentfault.com/q/1010000000713614