起因是做了一个套静态的网页,但老板突然要求添加一些后台功能,只好将项目导入到Springboot项目中实现相关功能,由于功能简单所以只用原生html实现了相关功能,但考虑到安全问题就添加了CSRF验证
一、添加依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>3.4.4</version></dependency><!-- Thymeleaf 依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
二、SecurityConfig 配置
SecurityConfig配置文件用于配置 Spring Security 的安全策略,它结合之前引入的spring - boot - starter - security依赖,为项目提供用户认证、授权以及 CSRF 防护等安全功能。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;// 声明该类为配置类,Spring 容器会扫描并加载其中的配置
@Configuration
// 启用 Spring Security 的 Web 安全功能,开启对 Web 请求的安全控制
@EnableWebSecurity
public class SecurityConfig {// 配置 Spring Security 的核心过滤器链,用于处理 HTTP 请求的安全相关逻辑@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {// 对 HTTP 请求进行授权配置http.authorizeHttpRequests(authz -> authz// 允许所有用户访问 "/login" 路径,因为登录页面需要公开访问,方便用户登录.requestMatchers("/login").permitAll()// 配置 "/upload.html", "/refLinks", "/getLinks","/saveLink","/deleteLink/**","/imageUpload.html" 这些路径// 只有经过认证(登录)的用户才能访问,这些通常是涉及敏感操作或数据的页面//其中/deleteLink/**为路径变量方式,如果选择这种传输数据到服务端就可以这样写.requestMatchers("/upload.html", "/refLinks", "/getLinks","/saveLink","/deleteLink/**","/imageUpload.html").authenticated()// 允许其他所有请求无需认证即可访问,这样可以保证一些公共资源或页面能够被正常访问.anyRequest().permitAll())// 配置表单登录相关属性.formLogin(form -> form// 指定登录页面的路径为 "/login",当未认证用户访问受保护资源时会被重定向到此页面.loginPage("/login")// 用户登录成功后默认跳转到 "/upload.html" 页面.defaultSuccessUrl("/upload.html")// 用户登录失败后重定向到 "/login?error",通常会在该页面展示错误信息.failureUrl("/login?error")// 允许所有用户访问登录相关操作,如提交登录表单等.permitAll())// 配置注销(登出)相关操作,允许所有用户进行注销操作.logout(LogoutConfigurer::permitAll);return http.build();}// 配置用户详情服务,用于加载用户信息,如用户名、密码、角色等@Beanpublic UserDetailsService userDetailsService() {// 获取密码编码器,用于对用户密码进行加密处理PasswordEncoder passwordEncoder = passwordEncoder();// 创建一个用户实例,设置用户名、经过加密的密码和角色UserDetails user = User.builder()//用户名.username("user")// 密码,这里使用密码编码器对密码 "password" 进行加密.password(passwordEncoder.encode("password"))//角色.roles("USER").build();// 使用 InMemoryUserDetailsManager 将用户存储在内存中,// 适用于简单的测试或小型应用场景return new InMemoryUserDetailsManager(user);}// 配置密码编码器,用于对用户密码进行加密处理@Beanpublic PasswordEncoder passwordEncoder() {// 使用 BCryptPasswordEncoder,它是一种强密码哈希算法,安全性较高return new BCryptPasswordEncoder();}
}
/deleteLink/**:传输数据的方式为路径变量,双星号 ** 也是通配符,比单星号更强大,能匹配任意数量的路径层级,也就是可以跨层级匹配任意内容,如果使用这种方式传输数据就需要这样写。
三、Controller层代码
这段代码的作用是用户访问相关路径时候给页面添加CSRF验证
这里使用了Thymeleaf 渲染页面,所以return “login”;就可以打开对应的login.html页面
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;@Controller
public class PageController {@GetMapping("/login")public String showLoginPage(HttpServletRequest request, Model model) {CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());if (csrfToken != null) {model.addAttribute("_csrf", csrfToken);}return "login";}@GetMapping("/upload.html")public String upload(HttpServletRequest request, Model model) {CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());if (csrfToken != null) {model.addAttribute("_csrf", csrfToken);}return "upload";}@GetMapping("/upload")public String uploadWithOutHtml(HttpServletRequest request, Model model) {CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());if (csrfToken != null) {model.addAttribute("_csrf", csrfToken);}return "upload";}@GetMapping("/imageUpload.html")public String imageload(HttpServletRequest request, Model model) {CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());if (csrfToken != null) {model.addAttribute("_csrf", csrfToken);}return "imageUpload";}
}
四、前端html页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登录</title><!-- 存储 CSRF 令牌,Thymeleaf 条件渲染,有 _csrf 对象时才显示 --><meta name="_csrf" th:content="${_csrf.token}" th:if="${_csrf}"><!-- 存储 CSRF 令牌对应的请求头名称,Thymeleaf 条件渲染 --><meta name="_csrf_header" th:content="${_csrf.headerName}" th:if="${_csrf}">
</head>
<body><!-- 登录表单,提交到 /login 路径,POST 方法 --><form action="/login" method="post"><label for="username">用户名:</label><input type="text" id="username" name="username" required><br><label for="password">密码:</label><input type="password" id="password" name="password" required><br><!-- 隐藏输入框存储 CSRF 令牌,提交表单时发送给服务器 --><input type="hidden" id="csrfToken" name="_csrf" value=""><button type="submit">登录</button></form><p style="color: red;" id="errorMessage" hidden>用户名或密码错误</p><script>// 获取 CSRF 令牌 meta 标签const csrfMeta = document.querySelector('meta[name="_csrf"]');// 提取 CSRF 令牌值const csrfToken = csrfMeta? csrfMeta.getAttribute('content') : '';// 获取隐藏输入框元素const csrfInput = document.getElementById('csrfToken');// 设置隐藏输入框的值为 CSRF 令牌if (csrfInput) {csrfInput.value = csrfToken;}const urlParams = new URLSearchParams(window.location.search);if (urlParams.has('error')) {document.getElementById('errorMessage').hidden = false;}</script><!-- CSRF 验证原理注释:1. 服务器生成唯一的 CSRF 令牌,通过 meta 标签传给前端。2. 前端在表单提交时,将令牌放入隐藏输入框或请求头发送给服务器。3. 服务器接收请求,对比请求中的令牌和服务器端存储的令牌。4. 若令牌一致,认为请求合法;不一致则拒绝请求,防范跨站请求伪造攻击。-->
</body>
</html>
标签定义了一个 HTML 表单。
- action=“/login”:表明表单数据会被提交到 /login 这个 URL。
- method=“post”:意味着使用 HTTP POST 方法来提交表单数据。
表单内有三个输入字段: - 用户名输入框(id=“username”,name=“username”)。
- 密码输入框(id=“password”,name=“password”)。
- 隐藏输入框(id=“csrfToken”,name=“_csrf”),此输入框用于存储 CSRF 令牌。
登录:是一个提交按钮,点击该按钮就会触发表单提交操作。
// 获取 CSRF 令牌并设置到隐藏字段中
const csrfMeta = document.querySelector('meta[name="_csrf"]');
const csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
const csrfInput = document.getElementById('csrfToken');
if (csrfInput) {csrfInput.value = csrfToken;
}
JavaScript 代码的作用是从 标签里获取 CSRF 令牌,然后把这个令牌值赋给隐藏输入框 csrfToken。这样,在表单提交时,CSRF 令牌就会作为表单数据的一部分被发送到服务器。
请求发送
当你点击 “登录” 按钮时,浏览器会自动把表单中的数据(包含用户名、密码和 CSRF 令牌)以 POST 方法发送到 /login 这个 URL。在这个过程中,并没有显式的 JavaScript 代码来发送请求,而是利用了 HTML 表单的默认行为。
浏览器会根据 元素的 method 和 action 属性来发送 HTTP 请求。
- method 属性:指定请求的方法,常见的值有 GET 和 POST。在代码中,method=“post” 表示使用 POST 方法发送请求。
- action 属性:指定请求的目标 URL。在你的代码中,action=“/login” 表示请求会发送到 /login 这个 URL。
HTML 标准对 元素和提交按钮的行为进行了明确规定。当 元素内的提交按钮( 或 )被点击时,浏览器会自动收集表单内所有具有 name 属性的表单控件(如 、、 等)的值,并按照特定的格式将这些值组合成请求数据,然后根据 元素的 action 和 method 属性来发送 HTTP 请求。
根据表单的 enctype 属性(默认值为
application/x-www-form-urlencoded),浏览器会对收集到的表单数据进行编码。对于
application/x-www-form-urlencoded 编码,键值对之间用 & 连接,特殊字符会进行 URL
编码。上述表单数据编码后会变成
username=exampleUser&password=examplePassword&_csrf=abc123。