我们相信:世界是美好的,你是我也是。 来玩一下解压小游戏吧!

对于"用户接口请求/表单提交"来说,实际上存在着普遍的csrf攻击问题。这个问题是如何产生的呢?应该如何防范呢?基于springboot的项目里面,如何简单的增加相关防范代码呢?这就是本文所试图解决的问题。

苏南大叔:SpringBoot开发,接口请求,如何增加CSRF防护? - springboot-csrf
SpringBoot开发,接口请求,如何增加CSRF防护?(图4-1)

苏南大叔的“程序如此灵动”博客,记录苏南大叔的代码编程经验总结。测试环境:win10openjdk@23.0.2IntelliJ IDEA 2024.3.4.1maven@3.3.2spring boot@2.5.4java@17mysql@5.7.26

csrf基本原理

csrf的基本原理是这样的:

csrf攻击

跨站请求伪造(Cross-Site Request Forgery,简称 CSRF)是一种攻击方式,攻击者通过伪造用户的请求,利用用户在目标网站的身份进行未授权的操作。CSRF攻击通常发生在用户已经登录目标网站并且会话仍然有效的情况下。

对于被攻击的网站来说,其实并没有太大的过错,接口都是可以正常调用的,甚至登陆session检测也是存在的。但是,它是如何被利用的呢?

第三方网站知晓这些网站接口的调用方式后,就可以在访问第三方网站的时候,自动执行这些接口。由于浏览器的机制,【某些特殊情况下】会自动带上正常的cookie,从而导致接口端误认为这个是正常的用户行为。

csrf攻击行为和正常的用户行为,唯一不同的地方就是:referer!来源页不同。所以,如果服务器端对referer做严格检测的话,应该是可以区分是否是用户正常行为的。

自动带上cookie这件事情,和浏览器有关。很古老版本的浏览器中,都是自动无条件带上cookie的。而目前的浏览器中,只有一些服务器上设置有问题的情况下,才会自动带上cookie。所以,原来普遍存在的csrf漏洞,目前变成了:个别情况下,才会有漏洞存在。转变的根本原因,就是:浏览器转变了对与cookie是否提交的默认处理方式。

csrf防护

防护措施,除了常规的referer检测外,最常见的方案就是增加csrf token。实际上就是先由服务器端生成一个临时的随机字符串,保存在客户端。随着接口的提交,服务端要检测csrf token是否原装。非原装或者错误的值,就认为这是个非法请求。

对于仅返回数据这种并不修改数据的接口,一遍不做csrf检测,因为没有太大的必要。判断标准是依据接口行为来判断的,是否涉及数据的修改就是最好的标准。

目前还有一种方式,就是设置cookiesamesite属性。当然,默认值就是比较安全的lax。所以,如果设置了,可能会出问题。不设置,反而是安全的。

springboot与csrf

spring系列代码中,spring security自带csrf防护。然而,由于其天生的复杂性,太难以控制。所以苏南大叔并不建议使用spring security自带的csrf防护。

在本文中,就是使用传统概念上的csrf防护逻辑(并不是框架里面带的)。服务器端生成随机字符串,保存在session中。然后传递到客户端代码中,客户端再向服务端做请求的时候,在合适的时机,服务端再次检测这个随机字符串。

基础代码

项目代码比较多,所以本文仅叙述关键代码。其它代码可以参考以前的文章:

实际上主要的思路,还是session。登陆成功后,显示/success页面,该页面上,增加对/user接口的各种调用。

生成csrf token

LoginController.java文件里面,把随机字符串(uuid)写入到session里面。并且传递到模版里面。
\src\main\java\com\example\demo\controller\LoginController.java:

package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Controller
public class LoginController {
    //...
    @GetMapping("/success")
    public String showSuccessPage(HttpSession session, Model model) {
        if (session.getAttribute("user") == null) {
            return "redirect:/login";
        }
        model.addAttribute("user", session.getAttribute("user"));
        // HttpSession session = request.getSession();
        String csrfToken = UUID.randomUUID().toString();
        session.setAttribute("CSRF_TOKEN", csrfToken);
        model.addAttribute("csrfToken", csrfToken);
        return "success";
    }
}
Java

苏南大叔:SpringBoot开发,接口请求,如何增加CSRF防护? - 生成随机token
SpringBoot开发,接口请求,如何增加CSRF防护?(图4-2)

传递csrf token

src/main/resources/templates/success.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <form id="userForm">
        <input type="hidden" id="csrfToken" th:value="${csrfToken}">
        <input type="text" id="userName" placeholder="Name">
        <input type="password" id="userPassword" placeholder="Password">
        <button type="button" onclick="createUser()">Create User</button>
    </form>
    <div id="errorMessage" style="color: red;"></div>
    <script>
        function getCsrfToken() {
            return document.getElementById('csrfToken').value;
        }
        function createUser() {
            const name = document.getElementById('userName').value;
            const password = document.getElementById('userPassword').value;
            const csrfToken = getCsrfToken();
            fetch('/users', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': csrfToken
                },
                body: JSON.stringify({ name, password })
            })
            .then(response => {
                if (!response.ok) {
                    return response.text().then(text => { throw new Error(text); });
                }
                return response.json();
            })
            .then(data => {
                console.log('User created:', data);
                loadUsers();
                document.getElementById('errorMessage').innerText = '';
            })
            .catch(error => {
                document.getElementById('errorMessage').innerText = error.message;
            });
        }
    </script>
</body>
</html>
HTML

苏南大叔:SpringBoot开发,接口请求,如何增加CSRF防护? - csrf token
SpringBoot开发,接口请求,如何增加CSRF防护?(图4-3)

验证csrf token

src/main/java/com/example/demo/controller/UserController.java

package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;
    //...
    @PostMapping
    public ResponseEntity<?> createUser(@RequestBody User user, HttpServletRequest request) {
        if (!isLoggedIn(request)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("User not logged in");
        }
        try {
            validateCsrfToken(request);
            return ResponseEntity.ok(userService.createUser(user));
        } catch (RuntimeException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    }
    private void validateCsrfToken(HttpServletRequest request) {
        String csrfToken = request.getHeader("X-CSRF-TOKEN");
        String sessionCsrfToken = (String) request.getSession().getAttribute("CSRF_TOKEN");
        if (csrfToken == null || !csrfToken.equals(sessionCsrfToken)) {
            throw new RuntimeException("Invalid CSRF Token");
        }
    }
    private boolean isLoggedIn(HttpServletRequest request) {
        return request.getSession().getAttribute("user") != null;
    }
}
Java

苏南大叔:SpringBoot开发,接口请求,如何增加CSRF防护? - csrf检测
SpringBoot开发,接口请求,如何增加CSRF防护?(图4-4)

结语

本文描述的是不使用spring security,而是使用传统思路解决csrf问题。更多苏南大叔的java相关文章,可以参考下面的链接:

如果本文对您有帮助,或者节约了您的时间,欢迎打赏瓶饮料,建立下友谊关系。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。

 【福利】 腾讯云最新爆款活动!1核2G云服务器首年50元!

 【源码】本文代码片段及相关软件,请点此获取更多信息

 【绝密】秘籍文章入口,仅传授于有缘之人   java