最近接手了一个企业内部管理系统,安全模块基于 Spring Security 构建。虽然之前用过不少认证框架,但深入 Spring Security 的配置细节时还是踩了不少坑。于是决定从头搭建一个学习项目,把核心概念逐个吃透。这篇文章就是我的学习笔记。
项目地址:github.com/ov-vo/spring-security-learning
技术栈:Java 21 + Spring Boot 3.2.4 + Spring Security 6.x + Thymeleaf + Maven
一、Spring Security 是什么
简单来说,Spring Security 是 Spring 生态中的安全框架,负责处理两大核心问题:
- 认证(Authentication)——你是谁?验证用户身份
- 授权(Authorization)——你能做什么?控制用户权限
它通过一系列 过滤器(Filter) 拦截 HTTP 请求,在请求到达 Controller 之前完成身份验证和权限检查。这些过滤器的集合被称为 SecurityFilterChain——这是理解 Spring Security 最重要的概念。
二、项目初始化
创建一个 Spring Boot 项目,在 pom.xml 中引入以下核心依赖:
<!-- 核心安全依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Web 支持 -->
<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-devtools</artifactId>
<scope>runtime</scope>
</dependency>
项目结构保持简洁:
spring-security-learning/
├── pom.xml
├── src/main/java/com/example/security/
│ ├── SecurityLearningApplication.java # 启动类
│ ├── config/
│ │ └── SecurityConfig.java # ⭐ 安全配置核心
│ └── controller/
│ ├── DemoController.java # 演示不同权限等级的接口
│ └── LoginController.java # 登录页控制器
└── src/main/resources/
├── application.properties
└── templates/
└── login.html # 自定义登录页面
三、核心配置:SecurityConfig
这是整个项目的心脏。Spring Security 6.x 使用基于 Lambda 的 DSL 来配置安全规则,废弃了旧版的 WebSecurityConfigurerAdapter。
3.1 SecurityFilterChain —— 安全过滤器链
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 按 URL 规则授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // 公开访问
.requestMatchers("/admin/**").hasRole("ADMIN") // 仅管理员
.anyRequest().authenticated() // 其余需要登录
)
// 2. 表单登录
.formLogin(form -> form
.loginPage("/login") // 自定义登录页 URL
.permitAll() // 登录页允许所有人访问
)
// 3. HTTP Basic 认证(方便 curl 测试)
.httpBasic(Customizer.withDefaults())
// 4. 学习阶段关闭 CSRF(生产环境请务必开启!)
.csrf(csrf -> csrf.disable());
return http.build();
}
}
让我逐条解释这段配置:
🔓 permitAll()
不需要任何认证即可访问。适用于首页、静态资源、注册/登录页。
🔐 authenticated()
只要登录了就能访问,不检查角色。适用于需要登录但无特殊权限要求的页面。
👑 hasRole("ADMIN")
必须登录且持有 ADMIN 角色。Spring Security 会自动在角色名前加 ROLE_ 前缀,所以 hasRole("ADMIN") 实际匹配的是 ROLE_ADMIN。
此外还有 hasAuthority()(精确匹配权限名,不加前缀)、hasAnyRole()(满足任一角色即可)等变体。
3.2 PasswordEncoder —— 密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
BCrypt 是目前主流的密码哈希算法,自带盐值(salt),能有效抵御彩虹表攻击。永远不要把明文密码存入数据库或写在代码里——即使是学习项目,也建议从第一天就养成使用 BCrypt 的习惯。
3.3 UserDetailsService —— 用户数据源
这个 Bean 定义了"用户从哪里来"。学习阶段我们用内存用户:
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 普通用户
manager.createUser(User.withUsername("user")
.password(encoder.encode("password"))
.roles("USER")
.build());
// 管理员
manager.createUser(User.withUsername("admin")
.password(encoder.encode("password"))
.roles("USER", "ADMIN")
.build());
return manager;
}
这里展示了两个关键设计:
encoder.encode("password")—— 存储前先对密码做 BCrypt 哈希- admin 用户同时拥有
USER和ADMIN角色,这意味着 admin 可以访问所有 user 能访问的资源
在实际项目中,你会把这个 InMemoryUserDetailsManager 替换成基于数据库的 JdbcUserDetailsManager,或者自己实现 UserDetailsService 接口来对接任何用户存储(数据库、LDAP、外部 API 等)。
四、控制器:验证权限分层
为了直观感受权限控制的效果,我编写了三个层级的接口:
公开接口 —— 无需登录
@RestController
public class DemoController {
@GetMapping("/public/hello")
public String publicHello() {
return "Hello, 这是一个公开接口!任何人都可以访问。";
}
}
用户接口 —— 需要登录但不限角色
@GetMapping("/user/hello")
public Map<String, Object> userHello() {
return Map.of(
"message", "你好,认证用户!",
"username", getCurrentUsername(),
"roles", getCurrentRoles()
);
}
@GetMapping("/user/me")
public Map<String, Object> currentUser() {
Authentication auth = SecurityContextHolder
.getContext().getAuthentication();
return Map.of(
"username", auth.getName(),
"authorities", auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList(),
"authenticated", auth.isAuthenticated()
);
}
这里展示了如何在代码中获取当前用户信息——通过 SecurityContextHolder.getContext().getAuthentication(),这是 Spring Security 的安全上下文,存储着当前请求的用户认证信息。整个调用链是这样工作的:
SecurityContextHolder ← 全局持有者(基于 ThreadLocal)
└── SecurityContext ← 当前请求的安全上下文
└── Authentication ← 认证对象
├── getName() → "user"
├── getAuthorities() → [ROLE_USER]
└── isAuthenticated() → true
管理员接口 —— 需要 ADMIN 角色
@GetMapping("/admin/hello")
public Map<String, Object> adminHello() {
return Map.of(
"message", "欢迎,管理员!",
"username", getCurrentUsername()
);
}
@GetMapping("/admin/system-info")
public Map<String, Object> systemInfo() {
return Map.of(
"os.name", System.getProperty("os.name"),
"java.version", System.getProperty("java.version"),
"availableProcessors", Runtime.getRuntime().availableProcessors()
);
}
/admin/system-info 是一个很好的实战示例——暴露敏感的服务器信息。在生产环境中,这类接口必须严格限制访问权限。
五、自定义登录页
Spring Security 自带一个简陋的默认登录页。但对于任何正式项目来说,自定义登录页是必选。我的登录页通过 Thymeleaf 模板渲染:
@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage() {
return "login"; // 对应 templates/login.html
}
}
<!-- templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录 - Spring Security Learning</title>
<style>
body { font-family: system-ui; display: flex;
justify-content: center; align-items: center;
min-height: 100vh; background: #f5f5f7; }
.login-box { background: white; padding: 40px;
border-radius: 16px; box-shadow: 0 4px 24px rgba(0,0,0,.06);
width: 360px; }
input { width: 100%; padding: 10px 14px; margin: 8px 0;
border: 1px solid #d2d2d7; border-radius: 8px; font-size: 15px; }
button { width: 100%; padding: 12px; background: #0071e3;
color: white; border: none; border-radius: 8px;
font-size: 16px; font-weight: 600; cursor: pointer; }
</style>
</head>
<body>
<div class="login-box">
<h2 style="text-align:center">🔐 系统登录</h2>
<!-- 测试账号提示 -->
<div style="background:#f0f7ff; padding:12px; border-radius:8px;
margin:16px 0; font-size:13px; color:#555;">
<strong>测试账号</strong><br>
普通用户:user / password<br>
管理员:admin / password
</div>
<form th:action="@{/login}" method="post">
<input type="text" name="username"
placeholder="用户名" required>
<input type="password" name="password"
placeholder="密码" required>
<button type="submit">登 录</button>
</form>
</div>
</body>
</html>
这里有两个关键约定:
- 表单的
action必须指向@{/login},且method必须为POST - 用户名和密码的输入框
name属性必须是username和password(这是 Spring Security 的默认字段名)
六、实际测试
启动项目后,你可以用 curl 快速验证各种场景:
# 1. 访问公开接口 —— 无需认证 ✅
curl http://localhost:8080/public/hello
# → Hello, 这是一个公开接口!
# 2. 访问用户接口 —— 需要认证(不带凭据)❌
curl http://localhost:8080/user/hello
# → 返回 401 / 重定向到登录页
# 3. 用 Basic Auth 访问用户接口 ✅
curl -u user:password http://localhost:8080/user/hello
# → {"message":"你好,认证用户!","username":"user","roles":"ROLE_USER"}
# 4. 普通用户访问管理接口 ❌ 403 Forbidden
curl -u user:password http://localhost:8080/admin/system-info
# → 返回 403
# 5. 管理员访问管理接口 ✅
curl -u admin:password http://localhost:8080/admin/system-info
# → {"os.name":"Mac OS X","java.version":"21.0.2",...}
第 4 个测试尤其值得注意:user 已经通过了认证,但因为缺少 ADMIN 角色,返回的是 403 Forbidden 而非 401 Unauthorized。这完美展示了"认证"和"授权"是两个独立的概念。
七、踩坑记录
坑 1:CSRF 导致 POST 请求 403
如果你的登录表单提交后一直返回 403,很可能是 CSRF 保护在捣乱。Spring Security 默认开启 CSRF 保护,要求所有 POST 请求携带 CSRF token。学习阶段可以暂时关闭,但上线前务必重新开启:
// 学习时关闭
http.csrf(csrf -> csrf.disable());
// 上线时恢复(删除这行即可,默认就是开启的)
// 并在 Thymeleaf 表单中加上:
// <input type="hidden" th:name="${_csrf.parameterName}"
// th:value="${_csrf.token}" />
坑 2:Spring Security 6.x 的 API 变化
如果你参考的是旧版教程,可能会看到 WebSecurityConfigurerAdapter 的用法。这个类在 Spring Security 5.7+ 被标记为弃用,在 6.x 中已完全移除。新版本统一使用 基于 Bean 的配置:定义 SecurityFilterChain Bean 并在其中使用 Lambda 表达式配置规则。
坑 3:角色名自动加 ROLE_ 前缀
使用 .hasRole("ADMIN") 时,Spring Security 自动在前面加上 ROLE_,变成 ROLE_ADMIN。如果你在 .roles() 里写成 "ROLE_ADMIN",最终会变成 ROLE_ROLE_ADMIN——这个坑已经坑了无数人。
八、学习路线图
这个项目只是 Spring Security 的入门第一步。基于这个基础,后续可以逐步深入:
- 数据库用户存储 —— 用 Spring Data JPA + MySQL/PostgreSQL 替代内存用户,实现
UserDetailsService接口 - 注册与邮箱验证 —— 实现完整的注册流程,加入邮箱激活
- JWT 无状态认证 —— 前后端分离场景下,用 JWT 替代 Session
- OAuth2 / OIDC —— 接入 GitHub / Google 第三方登录
- 方法级安全 —— 使用
@PreAuthorize、@PostAuthorize做细粒度权限控制 - 自定义认证提供者 —— 实现
AuthenticationProvider对接企业已有的认证系统 - 安全测试 —— 用
@WithMockUser和MockMvc编写安全测试
总结
Spring Security 的配置看起来繁琐,但核心思想其实非常清晰:
过滤器链(FilterChain)拦截请求 → 认证管理器验证身份 → 投票器(Voter)决定是否有权限 → 放行或拒绝。
理解了这个流程,再回头看配置代码,每一行都变得有据可循。如果你也在学习 Spring Security,建议不要试图一上来就配置一个"完整的企业级安全方案"——那只会让你迷失在无数配置项中。像我这样从最小可运行项目开始,逐步添加功能,学习效率会高得多。
完整代码已开源在 GitHub,欢迎 clone 下来自己跑一跑,把每个接口用 curl 测一遍,理解会更加深刻。