Spring Security

Spring Security

想象一下,Spring Security 就像是你家小区的"超级保安队长"。在学习之前,我们要搞清楚两件事:

  1. 认证 (Authentication) :保安队长要看你的门禁卡(你是谁?),没卡不让进。
  2. 授权 (Authorization) :保安队长要看你的级别(你能去哪?),你是普通住户就不能进别墅区。

准备好了吗?让我们开始今天的"保安训练营"吧!😎


🛡️ 第一章:保安队长的"七侠五义" (数据模型)

要搞权限,光杆司令可不行,我们需要一套"江湖规矩"(数据库表)。文档里提到了 7 张核心表,它们的关系就像是一场复杂的"相亲局":

  • 核心人物 (角色表 t_role):这是 C 位!所有的关系都围绕它转。
  • 三对多对多关系
    • 用户 ↔ 角色 (User ↔ Role)
    • 角色 ↔ 权限 (Role ↔ Permission)
    • 角色 ↔ 菜单 (Role ↔ Menu)

保安队长的逻辑是这样的:

  1. 看人 (认证) :只查 t_user 表,确认你是张三李四。
  2. 定级 (授权) :查完你是谁,马上去查你的角色 ,再根据角色查你能看的菜单 和能用的权限

🧱 第二章: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 "删除成功!";
    }
}

📝 总结 (保安队长的一天)

  1. 拦截 :你一请求,FilterChainProxy 就把你拦下来。
  2. 查岗UsernamePasswordAuthenticationFilter 看你要不要登录。
  3. 验明正身 :去调用你写的 UserDetailsService,从数据库拿用户。
  4. 核对密码 :用 BCryptPasswordEncoder 核对你输入的密码和数据库里的密文。
  5. 发证 :给你发个 SecurityContext(就像手环),以后你带着手环就能通过后面的检查。
  6. 放行FilterSecurityInterceptor 看看你有没有权限进这个房间,有就放行,没有就 403!

搞定!收工!🍻

相关推荐
葫芦和十三4 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp5 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑5 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯6 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan8 小时前
多Agent之间的区别
后端
青石路10 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充10 小时前
1.面向对象设计思想
后端
IT_陈寒11 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro11 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗11 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端