background

This series of tutorials was prepared as an internal training resource for the team. We mainly experience various features of SpringSecurity in an experimental way.

Continuing with article 3-SpringSecurity: Custom Form item: Spring-security-form, we demonstrate a scenario where CSRF protection is turned on (CSRF:.csrf().disable() is turned off).

The core dependencies are Web, SpringSecurity and Thymeleaf:

<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>
Copy the code

According to the official website, the key to CSRF protection is that we send a request with a random number (CSRF token), and this random number will not be carried by the browser automatically (eg: Cookie will be carried by the browser automatically).

Experiment 0: CSRF protection during login

Obviously, our login request here is a POST method (SpringSecurity defaults to ignoring CSRF interception of “GET”, “HEAD”, “TRACE”, “OPTIONS” idempotence requests). The _cSRF parameter must be entered during login and submitted together with authentication information. Otherwise, 403 is reported.

  • Back-end security configuration (enabled by defaultCSRF)
@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); }}); }Copy the code
  • Front-end templates (added_csrfParameter) :
<form action="login" method="post">
    <span>The user name</span><input type="text" name="username" /> <br>
    <span>password</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="Login">
</form>
Copy the code

Note:

  1. Of course, in practice you can add_csrfThe parameter is submitted as a hidden field:<input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" hidden/>
  2. In fact, if we use the default login page, we can also see a hidden field in the page element:

Experiment 1: CSRF protection on the POST interface

A form is one way to send a POST request, but we can’t submit all of our other requests through a form. Let’s make an Ajax POST request using native JavaScript.

  • The backend interface
@Controller
public class HelloController {
    @RequestMapping("/")
    public String hello(a){
        return "index";
    }
    
    @PostMapping(value = "/ok")
    @ResponseBody
    public String ok(a) {
        return "ok post"; }}Copy the code
  • Front-end template (added 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">Add user</a>
  <a href="/user/query">Query the user</a>
  <a href="/logout">exit</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>
Copy the code

ParameterName, _csrF. token, _csrF_header, _csrF. Token, _csrF_header, _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;
}    
Copy the code

Experiment 2: CSRF protection on exit

Logout url after CSRF is enabled, asking /logout as a tag (GET) will result in 404. You can exit logout only in POST mode.

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 booleancustomLogoutSuccess; ./** * 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; }}Copy the code

POST requests can be sent in the form form or Ajax format with _cSRF parameter. Here, using the form form as an example, click the POST logout button to exit successfully:

<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>
Copy the code

Experiment 3: CSRF protection in front and back end separation

Here is a demonstration of how to implement a cSRF-protected security request for a back-end decoupage project by receiving _cSRF from the back end in the template engine.

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().

  • Back-end security configuration (ModifiedCSRFStorage type: 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); }}); }Copy the code
  • The front-end script
</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"));
    // Then you can use the "xSRF-token" to request the back-end POST interface
  </script>
</body>    
Copy the code

Note: Many students have a question: if a Cookie is automatically added to the request, then the attacker can get it again.

Because the information in the Cookie is invisible to the attacker and cannot be forged, although the Cookie is automatically carried by the browser, all the attacker can do is to use the Cookie, and the attacker does not know what is put in the Cookie. Therefore, the cSRF-token written in the Cookie can defend against CSRF. Compared with the default storage in Session, the CSRF-token written in the Cookie only changes the storage location.

When do I need to enable CSRF?

Official documents suggest that CSRF protection be enabled for all operations involving browser users.

Reference

  • Source Code: Github
  • SpringSecurity official documentation
  • SpringSecurity official API

If you have any questions or any bugs are found, please feel free to contact me.

Your comments and suggestions are welcome!

This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.