Spring Security
想象一下,Spring Security 就像是你家小区的"超级保安队长"。在学习之前,我们要搞清楚两件事:
- 认证 (Authentication) :保安队长要看你的门禁卡(你是谁?),没卡不让进。
- 授权 (Authorization) :保安队长要看你的级别(你能去哪?),你是普通住户就不能进别墅区。
准备好了吗?让我们开始今天的"保安训练营"吧!😎
🛡️ 第一章:保安队长的"七侠五义" (数据模型)
要搞权限,光杆司令可不行,我们需要一套"江湖规矩"(数据库表)。文档里提到了 7 张核心表,它们的关系就像是一场复杂的"相亲局":
- 核心人物 (角色表
t_role):这是 C 位!所有的关系都围绕它转。 - 三对多对多关系 :
- 用户 ↔ 角色 (User ↔ Role)
- 角色 ↔ 权限 (Role ↔ Permission)
- 角色 ↔ 菜单 (Role ↔ Menu)
保安队长的逻辑是这样的:
- 看人 (认证) :只查
t_user表,确认你是张三李四。 - 定级 (授权) :查完你是谁,马上去查你的角色 ,再根据角色查你能看的菜单 和能用的权限。
🧱 第二章:Spring Security 入门 (Hello, 保安!)
2.1 搭建环境
首先,我们要把这位"保安队长"请进家里(项目)。
Maven 依赖 (pom.xml):
xml
<!-- Web 启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 🚨 保安队长驾到! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
启动类:
java
@SpringBootApplication
@EnableWebSecurity // 开启保安模式
public class SecurityApp {
public static void main(String[] args) {
SpringApplication.run(SecurityApp.class, args);
System.out.println("保安队长已上线!");
}
}
神奇的现象:
当你引入这个依赖后,哪怕你只写了一个 index.html,访问时也会自动跳转到一个登录页!
- 默认用户名 :
user - 默认密码:在控制台启动日志里找(一串随机字符串)。
💡 小贴士 :这时候你可能会问:"登录页哪来的?"
答案是:这是保安队长自带的
DefaultLoginPageGeneratingFilter自动生成的,虽然丑,但是能用。
🕵️ 第三章:保安队长的"火眼金睛" (核心过滤器)
Spring Security 的核心是一个名为 springSecurityFilterChain 的超级过滤器。它内部其实藏着 15 个小弟(过滤器),各司其职。
我们挑几个最调皮的"小弟"认识一下:
| 小弟编号 | 外号 | 任务 |
|---|---|---|
| Filter 1 | SecurityContextPersistenceFilter |
"记事本":在你进门(请求)和出门(响应)时,帮你拿个本子记一下你是谁,存进 Session。 |
| Filter 6 | UsernamePasswordAuthenticationFilter |
"查岗王" :专门盯着 /login 的 POST 请求,负责验证用户名密码。 |
| Filter 10 | AnonymousAuthenticationFilter |
"气氛组":如果你没登录,它会给你发个"游客"牌子(匿名用户),让你也能走完流程,但干不了坏事。 |
| Filter 15 | FilterSecurityInterceptor |
"最终审判":在你动手(访问资源)前最后一秒,检查你有没有资格,没资格直接扔异常。 |
🛠️ 第四章:定制你的专属保安 (进阶配置)
默认的保安太死板,我们要定制!我们需要写一个配置类,继承 WebSecurityConfigurerAdapter。
4.1 基础配置类 (保安队长的说明书)
java
@Configuration
@EnableWebSecurity
// 为了后面能用注解控制权限,先把这个打开
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 开始说规矩
.antMatchers("/pages/a.html").permitAll() // a.html 谁都能看
.antMatchers("/pages/b.html").hasRole("VIP") // b.html 必须是 VIP
.anyRequest().authenticated() // 除了上面的,其他的都得登录才能看
.and()
.formLogin() // 启用表单登录(也就是自己写登录页)
.loginPage("/login.html") // 指定我们的登录页
.loginProcessingUrl("/login") // 指定处理登录的 URL
.defaultSuccessUrl("/index.html") // 登录成功去哪
.and()
.logout() // 启用退出功能
.logoutUrl("/logout") // 退出的 URL
.logoutSuccessUrl("/login.html") // 退出后去哪
.and()
.csrf().disable(); // 关闭防跨站请求伪造 (测试时关掉,不然 POST 请求会 403)
}
}
4.2 自定义登录页面
保安队长默认的登录页太丑了,我们要换一个!
在 resources/static 下建个 login.html:
html
<!DOCTYPE html>
<html>
<head>
<title>自定义登录页</title>
</head>
<body>
<h2>欢迎登录保安系统!</h2>
<!-- 注意:action 必须和上面配置的 loginProcessingUrl 一致 -->
<!-- 必须是 POST 方法,保安队长只认这个 -->
<form action="/login" method="post">
用户名: <input type="text" name="username"/><br>
密码: <input type="password" name="password"/><br>
<input type="submit" value="保安队长,放行!"/>
</form>
</body>
</html>
🔐 第五章:数据库实战与密码加密 (硬核环节)
5.1 从数据库拿用户 (UserDetailsService)
保安队长不会自己去数据库查人,他需要一个"情报员"(实现 UserDetailsService 接口)。
java
@Service
public class MyUserService implements UserDetailsService {
// 模拟数据库里的用户 (实际开发这里要调 Mapper)
private Map<String, UserInfo> userDb = new HashMap<>();
public MyUserService() {
// 初始化一个管理员
UserInfo admin = new UserInfo("admin", "123456");
userDb.put("admin", admin);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 去"数据库"查人
UserInfo userInfo = userDb.get(username);
if (userInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 给这个人发"权限卡" (注意:角色必须以 ROLE_ 开头)
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("add")); // 权限:新增
authorities.add(new SimpleGrantedAuthority("delete")); // 权限:删除
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); // 角色:管理员
// 3. 把人交给保安队长 (Spring 的 User 对象)
// 参数:用户名、密码、权限列表
return new User(username, userInfo.getPassword(), authorities);
}
}
5.2 密码加密 (BCryptPasswordEncoder)
以前我们存密码是明文的,这太危险了!现在我们要用 BCrypt 算法,它就像"乱炖",每次加密结果都不一样,但都能验出来。
1. 配置加密器 Bean:
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 引入 BCrypt 加密
}
}
2. 修改 UserService:
java
@Service
public class MyUserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder; // 注入加密器
@Override
public UserDetails loadUserByUsername(String username) {
// ...查库逻辑...
// 假设数据库里存的是加密后的密码
// String dbPassword = userFromDb.getPassword();
// 模拟:如果我们要存密码,应该这样存:
// String encodedPass = passwordEncoder.encode("123456");
// System.out.println(encodedPass); // 存入数据库
// 保安队长会自动用 passwordEncoder 去对比你输入的明文和数据库的密文
return new User(username, dbPassword, authorities);
}
}
BCrypt 的神奇之处:
java
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 这两串看起来不一样,但代表同一个密码!
String hash1 = encoder.encode("hello"); // $2a$10$vQwE...
String hash2 = encoder.encode("hello"); // $2a$10$abcX...
// 保安队长怎么验证?
boolean b1 = encoder.matches("hello", hash1); // true
boolean b2 = encoder.matches("hello", hash2); // true
🚀 第六章:注解式权限控制 (方法级防火墙)
有时候 URL 拦不住,我们要精确到具体的方法。这时候就要用到注解了!
1. 开关打开:
在配置类上加上 @EnableGlobalMethodSecurity(prePostEnabled = true)
2. 锁死方法:
java
@RestController
public class HelloController {
// 只有拥有 "add" 权限的人才能调用这个方法
@PreAuthorize("hasAuthority('add')")
@GetMapping("/doAdd")
public String doAdd() {
return "新增成功!";
}
// 只有拥有 "ROLE_ADMIN" 角色的人才能调用
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/doDelete")
public String doDelete() {
return "删除成功!";
}
}
📝 总结 (保安队长的一天)
- 拦截 :你一请求,
FilterChainProxy就把你拦下来。 - 查岗 :
UsernamePasswordAuthenticationFilter看你要不要登录。 - 验明正身 :去调用你写的
UserDetailsService,从数据库拿用户。 - 核对密码 :用
BCryptPasswordEncoder核对你输入的密码和数据库里的密文。 - 发证 :给你发个
SecurityContext(就像手环),以后你带着手环就能通过后面的检查。 - 放行 :
FilterSecurityInterceptor看看你有没有权限进这个房间,有就放行,没有就 403!
搞定!收工!🍻