SpringBoot开发,接口请求,如何增加CSRF防护?
发布于 作者:苏南大叔 来源:程序如此灵动~

对于"用户接口请求/表单提交"来说,实际上存在着普遍的csrf
攻击问题。这个问题是如何产生的呢?应该如何防范呢?基于springboot
的项目里面,如何简单的增加相关防范代码呢?这就是本文所试图解决的问题。
苏南大叔的“程序如此灵动”博客,记录苏南大叔的代码编程经验总结。测试环境:win10
,openjdk@23.0.2
,IntelliJ IDEA 2024.3.4.1
,maven@3.3.2
,spring boot@2.5.4
,java@17
,mysql@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
检测,因为没有太大的必要。判断标准是依据接口行为来判断的,是否涉及数据的修改就是最好的标准。
目前还有一种方式,就是设置cookie
的samesite
属性。当然,默认值就是比较安全的lax
。所以,如果设置了,可能会出问题。不设置,反而是安全的。
springboot与csrf
spring
系列代码中,spring security
自带csrf
防护。然而,由于其天生的复杂性,太难以控制。所以苏南大叔并不建议使用spring security
自带的csrf
防护。
在本文中,就是使用传统概念上的csrf
防护逻辑(并不是框架里面带的)。服务器端生成随机字符串,保存在session
中。然后传递到客户端代码中,客户端再向服务端做请求的时候,在合适的时机,服务端再次检测这个随机字符串。
基础代码
项目代码比较多,所以本文仅叙述关键代码。其它代码可以参考以前的文章:
- https://newsn.net/say/springboot.html
- https://newsn.net/say/springboot-hot.html
- https://newsn.net/say/springboot-model.html
- https://newsn.net/say/springboot-entity.html
- https://newsn.net/say/springboot-session.html
- https://newsn.net/say/springboot-jpa.html
实际上主要的思路,还是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";
}
}
传递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>
验证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;
}
}
结语
本文描述的是不使用spring security
,而是使用传统思路解决csrf
问题。更多苏南大叔的java
相关文章,可以参考下面的链接:


