🏢 把 Spring Security 想象成一栋大楼的保安系统
你的网站就是一栋大楼,里面有很多房间(页面/接口)。Spring Security 就是门口的保安。
保安只干两件事:
- 认证:你是谁?→ 出示工牌(登录)
- 授权:你能进哪个房间?→ 看你的工牌等级(角色权限)
就这么简单。下面一步步来。
第一步:不装保安 vs 装保安
不装保安(不加依赖):谁都能进任何房间,随便逛。
装了保安 (加 spring-boot-starter-security 依赖):你一启动项目,保安就自动上岗了。这时候你访问任何页面,都会被拦下来问:"你谁啊?先登录!"
保安会给你一个默认登录页。用户名固定是 user,密码在启动日志里打印出来(一串乱码)。你输对了才能进。
这一步你一行代码都不用写,保安就自动工作了。这就是 Spring Security 的默认行为。
第二步:告诉保安"有哪些人"
默认只有一个 user,肯定不够。你要给保安一份花名册,告诉他:
- 张三,密码 123456,普通员工
- 李四,密码 888888,管理员
这份花名册,在代码里就叫 UserDetailsService。你写一个方法,返回"张三、李四"这些人,保安就知道了。
关键词:花名册 = UserDetailsService。你只要记住"我要给保安一份名单"就行。
第三步:告诉保安"哪个房间谁能进"
保安拿到花名册后,还得知道规则。你要写一份门禁规则:
/public/开头的房间 → 谁都能进,不用出示工牌/admin/开头的房间 → 只有管理员能进- 其他房间 → 登录了就能进
这份规则写在 SecurityFilterChain 里。你只要记住:这里就是写"哪个门谁能进"的地方。
代码长这样(不用背,看个意思):
less
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // 公共房间,随便进
.requestMatchers("/admin/**").hasRole("ADMIN") // 管理员房间,查工牌
.anyRequest().authenticated() // 其他房间,登录就行
);
翻译成人话就是:公共的随便进,管理员的要管理员,剩下的登录了就能进。
第四步:密码为什么要"加密"
花名册里存密码,不能存明文 (比如直接存 123456),万一花名册被偷了,全完蛋。
所以保安要求:存进去之前,先把密码"搅碎"(加密)。等你登录时,他把你输入的密码也用同样的方法搅碎,比对两个碎渣是否一样。
这个"搅碎机"在代码里叫 PasswordEncoder,最常用的是 BCryptPasswordEncoder。
记住:密码必须搅碎存,不能存明文。
🎯 整个流程串起来(重点看这个)
bash
你访问 /admin/页面
↓
保安拦住:出示工牌!(认证)
↓
你输入 用户名+密码
↓
保安查花名册(UserDetailsService):有这个人吗?密码搅碎后对得上吗?
↓
对上了 → 发你一张通行证(SecurityContext 里存下你的信息)
↓
保安再看门禁规则(SecurityFilterChain):/admin/ 要 ADMIN 角色
↓
查你工牌:你是 ADMIN 吗?
├─ 是 → 放行,进房间 ✅
└─ 否 → 拦住,403 没权限 ❌
你只需要记住 3 个词
| 词 | 大白话 | 作用 |
|---|---|---|
| UserDetailsService | 花名册 | 告诉保安有哪些人、密码、角色 |
| SecurityFilterChain | 门禁规则 | 告诉保安哪个房间谁能进 |
| PasswordEncoder | 搅碎机 | 把密码加密,不明文存 |
认证 = 查你是谁(登录)
授权 = 查你能不能进(角色权限)
那你实际要写几段代码?
- 一个类,里面放花名册 (
UserDetailsService) - 同一个类里,放门禁规则 (
SecurityFilterChain) - 再配一个搅碎机 (
PasswordEncoder)
就这三样。其他都是细节。
具体怎么实现
下面是一份完整能跑的代码,照着做,跑起来就能看到效果。
📁 项目结构
css
src/main/java/com/example/demo/
├── DemoApplication.java (启动类,Spring Boot 自带,不用改)
├── SecurityConfig.java (★ 新写:花名册 + 门禁规则 + 搅碎机)
└── HelloController.java (★ 新写:几个测试页面)
1️⃣ 依赖(pom.xml)
只要加这一个,其他都是 Spring Boot 自带的:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2️⃣ SecurityConfig.java(核心,花名册 + 门禁 + 搅碎机)
java
package com.example.demo;
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.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;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// ========== 搅碎机:密码加密用 ==========
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// ========== 花名册:告诉保安有张三、李四 ==========
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
// 张三:普通员工,角色 USER
UserDetails zhangsan = User.withUsername("zhangsan")
.password(encoder.encode("123456")) // 密码搅碎后存
.roles("USER")
.build();
// 李四:管理员,角色 ADMIN
UserDetails lisi = User.withUsername("lisi")
.password(encoder.encode("888888"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(zhangsan, lisi);
}
// ========== 门禁规则:哪个房间谁能进 ==========
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // 公共房间,谁都能进
.requestMatchers("/admin/**").hasRole("ADMIN") // 管理员房间,只有李四能进
.requestMatchers("/user/**").hasRole("USER") // 员工房间,只有张三能进
.anyRequest().authenticated() // 其他房间,登录了就能进
)
.formLogin(form -> form
.permitAll() // 用默认登录页,登录页本身不用登录就能看
)
.logout(logout -> logout.permitAll()); // 允许退出登录
return http.build();
}
}
3️⃣ HelloController.java(测试房间)
kotlin
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
// 公共房间:不用登录就能进
@GetMapping("/public/hello")
public String publicHello() {
return "这是公共页面,谁都能看!";
}
// 员工房间:只有张三(USER)能进
@GetMapping("/user/hello")
public String userHello() {
return "这是员工页面,张三你好!";
}
// 管理员房间:只有李四(ADMIN)能进
@GetMapping("/admin/hello")
public String adminHello() {
return "这是管理员页面,李四你好!";
}
// 其他房间:登录了就能进(张三李四都行)
@GetMapping("/any")
public String anyPage() {
return "你登录了,所以能进这个页面。";
}
}
▶️ 怎么测试(照着点就行)
启动项目,然后在浏览器里依次访问:
① 访问 /public/hello
→ 直接显示"这是公共页面",不用登录 ✅
② 访问 /any
→ 自动跳到登录页。输入 zhangsan / 123456
→ 显示"你登录了",成功 ✅
③ 登录张三后,访问 /admin/hello
→ 显示 403 没权限 (因为张三是 USER,不是 ADMIN)❌
→ 这就是"授权"在起作用
④ 退出,换李四登录( lisi / 888888 ),访问 /admin/hello
→ 显示"这是管理员页面",成功 ✅
⑤ 李四访问 /user/hello
→ 403,因为李四不是 USER ❌
🎬 你会看到的现象
- 没登录时访问受保护页面 → 自动跳登录页(认证)
- 登录了但角色不对 → 403 拒绝(授权)
- 公共页面 → 直接放行(permitAll)
💡 对照"保安"理解
| 代码 | 保安里的角色 |
|---|---|
userDetailsService() |
花名册:张三、李四两人 |
passwordEncoder() |
搅碎机:密码加密 |
filterChain()里的 requestMatchers |
门禁规则:哪个房间谁能进 |
| 登录页输密码 | 认证:查你是谁 |
| 403 拒绝 | 授权:查你能不能进 |
跑完这 5 步,你就把 Spring Security 的认证 和授权都亲眼见过了。之后再学数据库查用户、JWT 令牌,都是在这套骨架上换零件而已。