[In-depth explanation of Spring Security (eight)] Separation of front and back ends - using CSRF vulnerability protection in detail

1. Overview of CSRF

CSRF(Cross-Site Request Forgery cross-site request forgery), also known as one-click attack (one-click-attack), usually abbreviated as CSRF or XSRF.

CSRFThe attack is an attack method that hijacks the user to send a malicious request on the currently logged-in browser . Compared with xss, which uses the user's trust in the specified website, CSRDit uses the website's trust in the user's web browser . To put it simply, CSRF is an attacker who tricks the user's browser through some technical means to visit a website that the user has authenticated and perform malicious requests, such as sending emails, sending messages, and even property operations (such as transferring money and purchasing goods) . Since the client (browser) has been authenticated in the website, the website will think that the real user is operating and execute the request (in fact, it is not the user's original intention).

Take a simple example:

Assuming that Xiaochai is now logged into a bank’s website to complete a transfer operation, the transfer link is as follows:

https://bank.xxx.com/withdraw?aaccount=小菜&amount=1000&for=myz

According to the request parameters of this link, it can be seen that this link is to transfer 1,000 yuan from Xiaochai’s account to myz account. Assuming that Xiaochai has not commented on the website of the bank, he opened a Dangerous website, there is a picture in this dangerous website, the code is as follows:

<img src="https://bank.xxx.com/withdraw?aaccount=小菜&amount=1000&for=zhangsan" />

Once the user opens this website, the request in this image link will be sent automatically. Since it is the same browser and the user has not logged out, the request will automatically carry the corresponding valid cookie information to complete a transfer operation.

This is CSRFCross Site Request Forgery.

Spring Security has CSRF defense turned on by default (if it is not turned off, it is turned on).
insert image description here

2. CSRF attack demonstration

Bank website: server.port=8080
transfer interface

@RestController
public class HelloController {
    
    

    @PostMapping("/withdraw")
    public String withdraw(){
    
    
        System.out.println("执行了一次转账操作");
        return "执行了一次转账操作";
    }

}

The website security configuration (CSRF defense off)

@EnableWebSecurity
public class SecurityConfig {
    
    

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    
        return http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                 .disable()
                // .and()
                .build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    
    
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailsService())
                .and()
                .build();
    }

    @Bean
    public UserDetailsService userDetailsService(){
    
    
        return new InMemoryUserDetailsManager(
                User.withUsername("root")
                        .password("{noop}123")
                        .roles("root")
                        .build()
        );
    }

}

Attack website: server.port=8081
Create a new index.html file in the resources/static directory, as follows:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模拟 CSRF 跨站请求伪造</title>
</head>
<body>

    <form action="http://localhost:8080/withdraw" method="post">
        <input name="name" type="hidden" value="小柴"/>
        <input name="money" type="hidden" value="10000"/>
        <input type="submit" value="点我"/>
    </form>

</body>
</html>

test

Please add a picture description

Note: Cookie information will only be carried if the same-origin policy is met.

3. CSRF defense

The root of CSRF attacks lies in the browser's default authentication mechanism (automatically carrying the cookie information of the current website, of course, in the case of satisfying the same-origin policy). Although this mechanism can guarantee that the request comes from a certain browser of the user, it cannot Make sure the request is authorized by the user. The request sent by the attacker and the user is exactly the same, which means that we have no way to directly reject a request here. If an additional parameter that cannot be obtained by the attacker can be carried in the legitimate request , the two different situations can be successfully distinguished, and the malicious request can be rejected directly. This mechanism is provided in Spring Security to defend against CSRF attacks, which we callToken Synchronization Mode

Token Synchronization Mode

This is the current mainstream CSRF attack defense scheme.The specific operation method is to provide a secure, randomly generated string in each HTTP request, in addition to the default Cookie parameters, which we call CSRF token. This CSRF token is generated by the server and saved in HttpSession after generation. When the front-end request arrives, compare the CSRF token information carried in the request with the token stored in the server, and if the two are not equal, reject the HTTP request.

Enable CSRF (of course it is enabled by default)
insert image description here
CSRF token generated by the server

insert image description here

4. Use CSRF for front-end and back-end separation

CsrfFilter source code analysis

In [An in-depth explanation of Spring Security (2)] The implementation principle of Spring Security, the editor explained the filters loaded by default, among which there is CsrfFiltera filter called , which is used to deal with CSRF attacks.

The following is the source code of its core method

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
    
    
		request.setAttribute(HttpServletResponse.class.getName(), response);
		// 从请求 Cookie 中找 csrf 令牌
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		// 是否存在令牌的标志
		boolean missingToken = (csrfToken == null);
		if (missingToken) {
    
    
			// 如果不存在的话就会去生成一个令牌,并存到 tokenRepository 仓库中
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		// 将 csrf 令牌放入 Request 作用域中
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
		// 判断请求方式,判断请求是否需要 CSRF 保护
		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;
		}
		// 从请求头中获取名为 X-XSRF-TOKEN 令牌值
		String actualToken = request.getHeader(csrfToken.getHeaderName());

		if (actualToken == null) {
    
    
		// 请求头中没有的话,就从请求参数中获取名为 _csrf 的令牌值
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
		// 将请求Cookie中的和请求参数/请求头中的 CSRF 令牌进行比对
		// 当然请求Cookie中不包含的话,会重新获取一个然后
		// 一致的话就放行,否则打印日志过滤掉该请求
		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);
	}

A detailed study of some methods in the source code

  • If the Cookie in the request does not contain CSRF token information, a CSRF token (in the form of Cookie) will be stored in the response and returned to the client through the method CsrfTokenRepositoryin .saveToken
    insert image description here

  • The request methods circled in the red box below do not participate in CSRF protection.
    insert image description here

  • If the Csrf token is put into the request header to process Csrf, the parameter name in the request header should be; X-XSRF-TOKENif the Csrf token is put into the request parameter to process Csrf, the parameter name in the request parameter should be _csrf.
    insert image description here

  • If the request method is satisfied, but there is no token corresponding to Csrf in the request parameters or request header, the request cannot be passed.
    insert image description here

test

When the front-end and back-end are separated and developed, the generated csrf token needs to be put into the cookie, and the token information in the cookie can be obtained and submitted when requesting (in the form of request header or request parameter).

Custom authentication filter to handle login authentication

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    
    


    public LoginFilter() {
    
    
    }

    public LoginFilter(AuthenticationManager authenticationManager) {
    
    
        super(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    
    

        if (!request.getMethod().equals("POST")) {
    
    
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        if(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){
    
    
            try {
    
    
                Map<String,String> loginInfo = JSONObject.parseObject(request.getInputStream(), Map.class);
                String username = loginInfo.get(getUsernameParameter());
                String password = loginInfo.get(getPasswordParameter());
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request,auth);
                return getAuthenticationManager().authenticate(auth);
            } catch (IOException e) {
    
    
                throw new RuntimeException(e);
            }
        }
        return super.attemptAuthentication(request, response);
    }
}

Security configuration information corresponding to Spring Security

@EnableWebSecurity
public class SecurityConfig {
    
    

    @Bean
    public UserDetailsService userDetailsService(){
    
    
        return new InMemoryUserDetailsManager(
                User.withUsername("root")
                        .password("{noop}123")
                        .roles("admin")
                        .build()
        );
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    
    
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailsService())
                .and()
                .build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    
        return http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .logout()
                .logoutUrl("/api/auth/logout")
                .and()
                .addFilterBefore(loginFilter(http), LogoutFilter.class)
                .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 将令牌保存到 Cookie 中,允许 Cookie 前端获取
                .and().build();
    }

    @Bean
    public LoginFilter loginFilter(HttpSecurity http) throws Exception {
    
    
        LoginFilter loginFilter = new LoginFilter(authenticationManager(http));
        loginFilter.setFilterProcessesUrl("/api/auth/login");
        loginFilter.setAuthenticationFailureHandler(this::onAuthenticationFailure);
        loginFilter.setAuthenticationSuccessHandler(this::onAuthenticationSuccess);
        return loginFilter;
    }

    @Resource
    private ObjectMapper objectMapper;
    private void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                         AuthenticationException exception) throws IOException, ServletException {
    
    
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.print(objectMapper.writeValueAsString(JsonData.failure(401,exception.getMessage())));
        out.close();
    }


    private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                         Authentication authentication) throws IOException, ServletException{
    
    
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        if(request.getRequestURI().endsWith("/login")){
    
    
            Map<String,Object> info = new HashMap<>();
            info.put("msg","登录成功");
            info.put("用户信息", SecurityContextHolder.getContext().getAuthentication().getPrincipal());
            out.write(objectMapper.writeValueAsString(JsonData.success(info)));
        }
        else if(request.getRequestURI().endsWith("/logout"))
            out.write(objectMapper.writeValueAsString(JsonData.success("注销成功")));
        // out.write(JSONObject.toJSONString(JsonData.success("登录成功")));
        out.close();
    }
}

Test Results

Input parameter test in request header

Please add a picture description

Input parameter test in Url request parameter

Please add a picture description

V. Summary

  • CSRF (Cross-Site Request Forgery), also known as one-click-attack, is usually abbreviated as CSRFor XSRF. A CSRF attack is an attack method that hijacks a user to send a malicious request on the currently logged-in browser.
  • Spring Security enables CSRF defense by default. When CSRF defense is enabled, there will be an additional CsrfFilterfilter named in the security filter chain SecurityFilterChain. This filter can be said to be the process of CSRF defense authentication.
  • To use CSRF vulnerability protection for front-end and back-end separation, you must first obtain the corresponding xsrf (that is, csrf) token information from the cookie. To enjoy server-side services, you need to configure this token information in the request parameter or request header (The request method here does not include GET, HEAD, OPTIONS, TRACE, if it is the four request methods, the filter will let it go directly), the configuration name in the request parameter _csrf, the value is the token information; the configuration name in the request header is X-XSRF-TOKEN, the value is the token information. This improves the security of the system to a certain extent. If you don't do it deliberately, this method largely prevents cross-site request forgery (CSRF).
  1. Carry the token in the request parameter
key: _csrf
value: "???"
  1. Carry the token in the request header
X-XSRF-TOKEN:value

Guess you like

Origin blog.csdn.net/qq_63691275/article/details/131148387