zoukankan      html  css  js  c++  java
  • SpringSecurity的防Csrf攻击

    CSRF(Cross-site request forgery)跨站请求伪造,也被称为One Click Attack或者Session Riding,通常缩写为CSRFXSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。 
    CSRF是一种依赖web浏览器的、被混淆过的代理人攻击(deputy attack)。

    如何防御

    使用POST请求时,确实避免了如img、script、iframe等标签自动发起GET请求的问题,但这并不能杜绝CSRF攻击的发生。一些恶意网站会通过表单的形式构造攻击请求

    public final class CsrfFilter extends OncePerRequestFilter {
        public static final RequestMatcher DEFAULT_CSRF_MATCHER = new
                CsrfFilter.DefaultRequiresCsrfMatcher();
        private final Log logger = LogFactory.getLog(this.getClass());
        private final CsrfTokenRepository tokenRepository;
        private RequestMatcher requireCsrfProtectionMatcher;
        private AccessDeniedHandler accessDeniedHandler;
        public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
            this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
            this.accessDeniedHandler = new AccessDeniedHandlerImpl();
            Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
            this.tokenRepository = csrfTokenRepository;
        }
        //通过这里可以看出SpringSecurity的csrf机制把请求方式分成两类来处理
        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);
    //第一类:"GET", "HEAD", "TRACE", "OPTIONS"四类请求可以直接通过
            if (!this.requireCsrfProtectionMatcher.matches(request)) {
                filterChain.doFilter(request, response);
            } else {
    //第二类:除去上面四类,包括POST都要被验证携带token才能通过
                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));
                    }
                } else {
                    filterChain.doFilter(request, response);
                }
            }
        }
        public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
            Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be
            null");
            this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
        }
        public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
            Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
            this.accessDeniedHandler = accessDeniedHandler;
        }
        private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
            private final HashSet<String> allowedMethods;
            private DefaultRequiresCsrfMatcher() {
                this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
        }
            public boolean matches(HttpServletRequest request) {
                return !this.allowedMethods.contains(request.getMethod());
            }
        }
    }

    禁用Csrf

    @EnableWebSecurity
    public class WebSecurityConfig extends
    WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
    //关闭打开的csrf保护
        .csrf().disable();
    }
    }

    Csrf Token

    用户登录时,系统发放一个CsrfToken值,用户携带该CsrfToken值与用户名、密码等参数完成登录。系统记录该会话的 CsrfToken 值,之后在用户的任何请求中,都必须带上该CsrfToken值,并由系统进行校验。
    这种方法需要与前端配合,包括存储CsrfToken值,以及在任何请求中(包括表单和Ajax)携带CsrfToken值。安全性相较于HTTP Referer提高很多,如果都是XMLHttpRequest,则可以统一添加CsrfToken值;但如果存在大量的表单和a标签,就会变得非常烦琐。

    SpringSecurity中使用Csrf Token

    Spring Security通过注册一个CsrfFilter来专门处理CSRF攻击,在Spring Security中,CsrfToken是一个用于描述Token值,以及验证时应当获取哪个请求参数或请求头字段的接口

    public interface CsrfToken extends Serializable {
        String getHeaderName();
        String getParameterName();
        String getToken();
    }
    //CsrfTokenRepository则定义了如何生成、保存以及加载CsrfToken。
    public interface CsrfTokenRepository {
        CsrfToken generateToken(HttpServletRequest request);
        void saveToken(CsrfToken token, HttpServletRequest request,
                       HttpServletResponse response);
        CsrfToken loadToken(HttpServletRequest request);
    }

     HttpSessionCsrfTokenRepository

    在默认情况下,Spring Security加载的是一个HttpSessionCsrfTokenRepository
    HttpSessionCsrfTokenRepository 将 CsrfToken 值存储在 HttpSession 中,并指定前端把CsrfToken 值放在名为“_csrf”的请求参数或名为“X-CSRF-TOKEN”的请求头字段里(可以调用相应的设置方法来重新设定)。校验时,通过对比HttpSession内存储的CsrfToken值与前端携带的CsrfToken值是否一致,便能断定本次请求是否为CSRF攻击。

    <input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}'>

    这种方式在某些单页应用中局限性比较大,灵活性不足。

    CookieCsrfTokenRepository

    Spring Security还提供了另一种方式,即CookieCsrfTokenRepository
    CookieCsrfTokenRepository 是一种更加灵活可行的方案,它将 CsrfToken 值存储在用户的cookie内。减少了服务器HttpSession存储的内存消耗,并且当用cookie存储CsrfToken值时,前端可以用JavaScript读取(需要设置该cookie的httpOnly属性为false),而不需要服务器注入参数,在使用方式上更加灵活。

    存储在cookie中是不可以被CSRF利用的,cookie 只有在同域的情况下才能被读取,所以杜绝了第三方站点跨域获取 CsrfToken 值的可能。CSRF攻击本身是不知道cookie内容的,只是利用了当请求自动携带cookie时可以通过身份验证的漏洞。但服务器对 CsrfToken 值的校验并非取自 cookie,而是需要前端手动将CsrfToken值作为参数携带在请求里

    下面是csrfFilter的过滤过程

    @Override
        protected void doFilterInternal(HttpServletRequest request,
                HttpServletResponse response, FilterChain filterChain)
                        throws ServletException, IOException {
            request.setAttribute(HttpServletResponse.class.getName(), response);
                    
                    //获取到cookie中的csrf Token(CookieTokenRepository)或者从session中获取(HttpSessionCsrfTokenRepository)
            CsrfToken csrfToken = this.tokenRepository.loadToken(request);
            final boolean missingToken = csrfToken == null;
                    //加载不到,则证明请求是首次发起的,应该生成并保存一个新的 CsrfToken 值
            if (missingToken) {
                csrfToken = this.tokenRepository.generateToken(request);
                this.tokenRepository.saveToken(csrfToken, request, response);
            }
            request.setAttribute(CsrfToken.class.getName(), csrfToken);
            request.setAttribute(csrfToken.getParameterName(), csrfToken);
    
                    //排除部分不需要验证CSRF攻击的请求方法(默认忽略了GET、HEAD、TRACE和OPTIONS)
            if (!this.requireCsrfProtectionMatcher.matches(request)) {
                filterChain.doFilter(request, response);
                return;
            }
    
                    //实际的token从header或者parameter中获取
            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;
            }
    
            filterChain.doFilter(request, response);
        }

    用户想要坚持CSRF Token在cookie中。 默认情况下CookieCsrfTokenRepository将编写一个名为 XSRF-TOKEN的cookie和从头部命名 X-XSRF-TOKEN中读取或HTTP参数 _csrf。

     
    //代码如下:
     
    .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())

    我们在日常使用中,可以采用header或者param的方式添加csrf_token,下面示范从cookie中获取token

    <form action="/executeLogin" method="post">
    <p>Sign in to continue</p>
    <div class="lowin-group">
        <label>用户名 <a href="#" class="login-back-link">Sign in?</a></label>
        <input type="text" name="username" class="lowin-input">
    </div>
    <div class="lowin-group password-group">
        <label>密码 <a href="#" class="forgot-link">Forgot Password?</a></label>
        <input type="password" name="password" class="lowin-input">
    </div>
    <div class="lowin-group">
        <label>验证码</label>
        <input type="text" name="kaptcha" class="lowin-input">
        <img src="/kaptcha.jpg" alt="kaptcha" height="50px" width="150px" style="margin-left: 20px">
    </div>
    <div class="lowin-group">
        <label>记住我</label>
        <input name="remember-me" type="checkbox" value="true" />
    </div>
    <input type="hidden" name="_csrf">
    <input class="lowin-btn login-btn" type="submit">
    </form>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>   
    <script>
        $(function () {
            var aCookie = document.cookie.split("; ");
            console.log(aCookie);
            for (var i=0; i < aCookie.length; i++)
            {
                var aCrumb = aCookie[i].split("=");
                if ("XSRF-TOKEN" == aCrumb[0])
                    $("input[name='_csrf']").val(aCrumb[1]);
            }
        });
    </script>

    注意事项

    springSecurity配置了默认放行, 不需要通过csrfFilter过滤器检测的http访问方式

        private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
            private final HashSet<String> allowedMethods = new HashSet<>(
                    Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
            @Override
            public boolean matches(HttpServletRequest request) {
                return !this.allowedMethods.contains(request.getMethod());
            }
        }

    之所以会有上面默认的GET,HEAD,TRACE,OPTIONS方式,是因为

    1. 如果这个http请求是通过get方式发起的请求,意味着它只是访问服务器 的资源,仅仅只是查询,没有更新服务器的资源,所以对于这类请求,spring security的防御策略是允许的;

    2. 如果这个http请求是通过post请求发起的, 那么spring security是默认拦截这类请求的
      因为这类请求是带有更新服务器资源的危险操作,如果恶意第三方可以通过劫持session id来更新 服务器资源,那会造成服务器数据被非法的篡改,所以这类请求是会被Spring security拦截的,在默认的情况下,spring security是启用csrf 拦截功能的,这会造成,在跨域的情况下,post方式提交的请求都会被拦截无法被处理(包括合理的post请求),前端发起的post请求后端无法正常 处理,虽然保证了跨域的安全性,但影响了正常的使用,如果关闭csrf防护功能,虽然可以正常处理post请求,但是无法防范通过劫持session id的非法的post请求,所以spring security为了正确的区别合法的post请求,采用了token的机制 。

  • 相关阅读:
    12.静态目录
    11.Git线上操作
    10.分离的前后台交互
    09.后台主页应用
    08.前端主页
    爬虫介绍
    python2与python3爬虫中get与post对比
    HTML+CSS+JavaScript
    数据库 Mysql-mongodb-redis
    时频工具箱介绍与使用
  • 原文地址:https://www.cnblogs.com/dalianpai/p/12393133.html
Copyright © 2011-2022 走看看