4-SpringSecurity:CSRF防护

简介: 4-SpringSecurity:CSRF防护

背景


本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验SpringSecurity的各项Feature。


接着上一篇文3-SpringSecurity:自定义Form表单中的项目:spring-security-form,继续演示开启CSRF防护的场景(当时关闭了CSRF:.csrf().disable())。


依赖不变,核心依赖为WebSpringSecurityThymeleaf

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

从官网中可以知道,CSRF防护的关键在于我们发请求时附带一个随机数(CSRF token),而这个随机数不会被浏览器自动携带(eg: Cookie就会被浏览器自动带上)。

image.png


实验0:登录时的CSRF防护


显然,我们这里的登录请求是个POST方法(SpringSecurity默认忽略"GET", "HEAD", "TRACE", "OPTIONS"等幂等请求的CSRF拦截)。登录时必须携带_csrf参数,与认证信息一并提交,否则报403。


  • 后端安全配置(默认开启CSRF
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            // .csrf().disable() // turn off csrf, or will be 403 forbidden
            .formLogin() // Support form and HTTPBasic
            .loginPage("/login")
            .failureHandler(new AuthenticationFailureHandler(){
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                    exception.printStackTrace();
                    request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
                }
            });            
}
  • 前端模板(新增了_csrf参数):
<form action="login" method="post">
    <span>用户名</span><input type="text" name="username" /> <br>
    <span>密码</span><input type="password" name="password" /> <br>
    <span>csrf token</span><input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> <br>
    <input type="submit" value="登录">
</form>

Note:


  1. 当然,实际中可以将新增的_csrf参数作为一个隐藏域进行提交:<input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" hidden/>


  1. 其实,如果我们使用默认的登录页面,可以在页面元素中看到同样有个隐藏域:

image.png


实验1:POST接口CSRF防护


通过form表单是一种发送POST请求的方式,但我们其他的请求不可能都通过form表单来提交。下面通过原生的JavaScript发起Ajax的POST请求。


  • 后端接口
@Controller
public class HelloController {
    @RequestMapping("/")
    public String hello(){
        return "index";
    }
    @PostMapping(value = "/ok")
    @ResponseBody
    public String ok() {
        return "ok post";
    }    
}
  • 前端模板(新增index.html)
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <meta name="csrf" th:content="${_csrf.token}">
  <meta name="_csrf_header" th:content="${_csrf.headerName}" />
  <title>SpringSecurity</title>
</head>
<body>
  <a href="/user/add">添加用户</a>
  <a href="/user/query">查询用户</a>
  <a href="/logout">退出</a>
  <script language="JavaScript">
    // let token = document.getElementsByTagName('meta')['csrf'].content;
    let token = document.querySelector('meta[name="csrf"]').getAttribute('content');
    let header = document.getElementsByTagName('meta')['_csrf_header'].content;
    console.log("token: ", token);
    console.log("header: ", header);
    function click() {
      let xhr = new XMLHttpRequest();
      xhr.open("POST", "http://localhost:8080/ok", true);
      xhr.setRequestHeader(header, token);
      xhr.onload = function (e) {
        console.log("response: ", e.target.responseText);
      }
      xhr.onerror = function (e) {
        console.log("error: ", e)
      }
      xhr.send(null);
    }
    click();
  </script>
</body>

image.png

image.png

Note: 前面这两个实验中用到了一些参数:_csrf.parameterName_csrf.token_csrf_header等,这些可以从源码中获悉:

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;
}


实验2:退出时的CSRF防护


退出url在开启CSRF之后,直接以a标签形式请求/logout(即GET方式)会报404;此时logout必须以POST方式才可以正常退出。

public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends
    AbstractHttpConfigurer<LogoutConfigurer<H>, H> {
  private List<LogoutHandler> logoutHandlers = new ArrayList<>();
  private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
  private String logoutSuccessUrl = "/login?logout";
  private LogoutSuccessHandler logoutSuccessHandler;
  private String logoutUrl = "/logout";
  private RequestMatcher logoutRequestMatcher;
  private boolean permitAll;
    private boolean customLogoutSuccess;
    ...
  /**
   * The URL that triggers log out to occur (default is "/logout"). If CSRF protection
   * is enabled (default), then the request must also be a POST. This means that by
   * default POST "/logout" is required to trigger a log out. If CSRF protection is
   * disabled, then any HTTP method is allowed.
   *
   * <p>
   * It is considered best practice to use an HTTP POST on any action that changes state
   * (i.e. log out) to protect against <a
   * href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF attacks</a>. If
   * you really want to use an HTTP GET, you can use
   * <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET"));</code>
   * </p>
   *
   * @see #logoutRequestMatcher(RequestMatcher)
   * @see HttpSecurity#csrf()
   *
   * @param logoutUrl the URL that will invoke logout.
   * @return the {@link LogoutConfigurer} for further customization
   */
  public LogoutConfigurer<H> logoutUrl(String logoutUrl) {
    this.logoutRequestMatcher = null;
    this.logoutUrl = logoutUrl;
    return this;
  }    
}

可采用form表单或者Ajax的形式发送POST请求,携带_csrf参数,这里以form表单为例,点击POST logout按钮,可成功退出:

<form action="logout" method="post">
    <input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" hidden/> <br>
    <input type="submit" value="POST logout">
</form>


实验3:前后端分离时的CSRF防护


前面是通过在模板引擎中接收后端传回的_csrf,这里演示下前后端分离项目如何实现CSRF防护下的安全请求。


A CsrfTokenRepository that persists the CSRF token in a cookie named "XSRF-TOKEN" and reads from the header "X-XSRF-TOKEN" following the conventions of AngularJS. When using with AngularJS be sure to use withHttpOnlyFalse().

  • 后端安全配置(修改CSRF存储类型:CookieCsrfTokenRepository)
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            // .csrf().disable() // turn off csrf, or will be 403 forbidden
            .formLogin() // Support form and HTTPBasic
            .loginPage("/login")
            .failureHandler(new AuthenticationFailureHandler(){
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                    exception.printStackTrace();
                    request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
                }
            });            
}
  • 前端脚本
</body>
  <script>
    function getCookie(name) {
      let arr = document.cookie.split("; ");
      for (let i = 0; i < arr.length; i++) {
        let arr2 = arr[i].split("=");
        if (arr2[0] == name) {
          return arr2[1];
        }
      }
      return "";
    }
    console.log("XSRF-TOKEN: ", getCookie("XSRF-TOKEN"));
    // 之后就可以拿着前面获取到的"XSRF-TOKEN"去请求后端POST等接口了
  </script>
</body>

image.png

Note: 这里大部分同学有个问题:Cookie都被自动带到请求中了,那攻击者不就又可以拿到了吗?


由于Cookie中的信息对于攻击者来说是不可见的,无法伪造的,虽然Cookie被浏览器自动携带了,但攻击者能做的仅仅是用一下Cookie,而Cookie里面到底放了什么内容,攻击者是不知道的,所以将CSRF-TOKEN写在Cookie中是可以防御CSRF的,相比默认的存放在Session中,CSRF-TOKEN写在Cookie中仅仅是换了一个存储位置。


什么时候需要开启CSRF?


image.png

官方文档建议,但凡涉及到浏览器用户操作,均应启用CSRF防护。

目录
相关文章
|
缓存 安全 Java
Spring Security的csrf防护措施|学习笔记
快速学习Spring Security的csrf防护措施
157 0
Spring Security的csrf防护措施|学习笔记
|
Web App开发 安全 JavaScript
CSRF(跨站请求伪造攻击)详解以及防护之道
CSRF(Cross Site Request Forgery, 跨站域请求伪造)是一种网络的攻击方式,该攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作,有很大的危害性。
1782 0
|
1月前
|
JavaScript 安全 前端开发
js开发:请解释什么是XSS攻击和CSRF攻击,并说明如何防范这些攻击。
XSS和CSRF是两种常见的Web安全威胁。XSS攻击通过注入恶意脚本盗取用户信息或控制账户,防范措施包括输入验证、内容编码、HTTPOnly Cookie和CSP。CSRF攻击则诱使用户执行未经授权操作,防范手段有CSRF Tokens、双重验证、Referer检查和SameSite Cookie属性。开发者应采取这些防御措施并定期进行安全审计以增强应用安全性。
22 0
|
5月前
|
安全 NoSQL Java
互联网并发与安全系列教程(06) - 常见的Web安全漏洞(CSRF攻击)
互联网并发与安全系列教程(06) - 常见的Web安全漏洞(CSRF攻击)
67 0
|
6月前
|
SQL 安全 前端开发
渗透攻击实例-邪恶的CSRF(社会工程学)
渗透攻击实例-邪恶的CSRF(社会工程学)
|
3天前
|
缓存 安全 JavaScript
前端安全:Vue应用中防范XSS和CSRF攻击
【4月更文挑战第23天】本文探讨了在Vue应用中防范XSS和CSRF攻击的重要性。XSS攻击通过注入恶意脚本威胁用户数据,而CSRF则利用用户身份发起非授权请求。防范措施包括:对输入内容转义、使用CSP、选择安全的库;采用Anti-CSRF令牌、同源策略和POST请求对抗CSRF;并实施代码审查、更新依赖及教育团队成员。通过这些实践,可提升Vue应用的安全性,抵御潜在攻击。
|
1天前
|
安全 前端开发 JavaScript
在Python Web开发过程中:Web框架相关,如何在Web应用中防止CSRF攻击?
在Python Web开发中防范CSRF攻击的关键措施包括:验证HTTP Referer字段、使用CSRF token、自定义HTTP头验证、利用Web框架的防护机制(如Django的`{% csrf_token %}`)、Ajax请求时添加token、设置安全会话cookie及教育用户提高安全意识。定期进行安全审计和测试以应对新威胁。组合运用这些方法能有效提升应用安全性。
3 0
|
7月前
|
安全 PHP 开发者
CSRF 攻击的防范措施
CSRF 攻击的防范措施
|
3月前
|
安全 前端开发 Java
什么是 CSRF 攻击?
什么是 CSRF 攻击?