通过实际demo掌握SpringSecurity+MP中的基本框架搭建

一. 前言

随着项目进一步开发,有些问题函待解决:

  • 用户多了就需要多个管理员和管理平台进行管理,如何区分?
  • 虽然可以使用if-else遇到管理员身份可以直接重定向,能不能优化?

所以打算通过SpringSecurity+MP+H2数据库实现一个小demo解决该问题

流程

主要流程

总体上demo用户认证的流程如下
用户提交表单
UsernamePasswordAuthenticationFilter
封装未认证令牌
ProviderManager
查询用户+密码比对
生成已认证Authentication
存入SecurityContextHolder
业务接口访问

二、主要框架

1.SecurityFilterChain

core

决定了一个请求需要经过哪些过滤器


  • Security5.7以后统一使用@Bean定义SecurityFilterChain

  • 所有的URL权限规则、登录页面配置、异常处理都是在这条链上进行处理

2.Authentication对象

是一个数据载体,对于当前项目,它包含了用户名与密码,经过一系列拦截器后包含了其他的信息要素


  • 它通过SecurityContextHolder获取当前登录用户

3.AuthenticationManager

负责接受Authentication请求,然后委托然后委托 给具体的 AuthenticationProvider 去处理


  • DaoAuthenticationProvider:专门负责核对数据库账号密码
  • OAuth2LoginAuthenticationProvider:专门负责核对第三方登录
  • 如果需要自定义特殊的登录逻辑,就需要自定义一个Provider

4.UserDetailsService&UserDetails

UserDetailsService负责根据用户名去数据库/Redis进行查找

UserDetails用户的信息,检查是否有有权限操作、是否过期


  • 需要实现loadUserByUsername方法,把数据库里的实体转换成UserDetails对象

5. PasswordEncoder

负责面的加密和匹配


  • Spring Security 强制要求密码不能明文存储。
  • 你必须定义一个 PasswordEncoder 的 Bean(推荐 BCryptPasswordEncoder)。
  • 登录时,AuthenticationProvider 会拿着用户输入的明文密码,用它算一遍,再和数据库里的密文比对。如果没配这个,登录永远报 "Bad Credentials"。

最终形成该流程
公开资源
受保护资源


用户请求
SecurityFilterChain
直接放行
是否登录?
跳转登录页
访问目标

三、搭建demo

1. 项目依赖 (pom.xml)

选择了轻量的 MP,同时为了方便集成使用,加入了H2数据库和AI简易生成的页面进行模拟

注意 : Spring Boot 3 需要搭配 mybatis-plus-spring-boot3-starter

xml 复制代码
<!-- 核心依赖片段 -->
<dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
<!--Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--Thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
<!--Thymeleaf Spring Security-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.27</version>
        </dependency>
<!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

2. 核心代码实现

2.1 实体类与 Mapper

使用 MP,我们只需继承 BaseMapper 即可获得全套 CRUD 能力,无需编写任何 XML 文件。

实体类
java 复制代码
//User.java
@Data
@TableName("users") // 指定表名
public class User {
    @TableId(type = IdType.AUTO) // 自增主键
    private Long id;

    private String username;
    private String password;
    private String role;
}

//Post.java
@Data
@TableName("posts")
public class Post {
    //自增主键
    @TableId(type = IdType.AUTO)
    private Long id;
    private String title;
    private String content;
    private String author;
}
java 复制代码
//UserMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

//PostMapper
@Mapper
public interface PostMapper extends BaseMapper<Post> {
}
2.2 自定义用户认证逻辑

Spring Security 需要通过 UserDetailsService 接口来加载用户信息。我们利用 MP 的 LambdaQueryWrapper 进行类型安全的查询。

实现类
java 复制代码
@Service
public class CustomerUserDetailsServiceImpl implements UserDetailsService {


    private final UserMapper userMapper;

    public CustomerUserDetailsServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    /**
     * 根据用户名查询用户信息
     * @param username 用户名
     * @return UserDetails 用户信息
     * @throws UsernameNotFoundException 用户名不存在
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername,username);

        //1.查询用户信息
        User user= userMapper.selectOne(wrapper);

        //2.如果不存在返回错误
        if(user==null)throw new UsernameNotFoundException("用户不存在"+username);

        //3.通过Userdetails的构造方法返回User实例
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                List.of(new SimpleGrantedAuthority(user.getRole()))
        );

    }
}

使用 User::getUsername 方法引用,避免了硬编码字符串字段名,重构更安全。

3.3 安全配置链 (SecurityFilterChain)

这是最关键的部分。我们需要明确告诉 Security:哪些路径公开,哪些需要登录,以及登录页面的地址。

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    //BCrypt编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 创建过滤器链
     * @param http HTTP安全配置对象
     * @param userDetailsService 自定义的用户详情服务
     * @return 配置好的安全过滤器链
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Bean
    public SecurityFilterChain filterChain(
            HttpSecurity http, // HttpSecurity 配置入口
            CustomerUserDetailsServiceImpl userDetailsService // 注入自定义用户认证逻辑
             )throws Exception {
        // 配置用户详情服务
        http
                //注册MP服务
                .userDetailsService(userDetailsService)

                //配置权限
                .authorizeHttpRequests(auth->auth

                        //requestMatchers()方法用于匹配请求,在这些URL中,任何用户都可以访问
                        .requestMatchers("/","/posts/","/login","/register","/h2-console/**","/css/**")

                        // permitAll()方法表示任何用户都可以访问
                        .permitAll()

                        // 匹配admin下的所有URL,只有ADMIN角色可以访问
                        .requestMatchers("/admin/**").hasRole("ADMIN")

                        // 匹配user下的所有URL,USER和ADMIN角色都可以访问
                        .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")

                        // 匹配任何URL, authenticated()方法限制只有登录的用户才能访问
                        .anyRequest().authenticated()
                )
                //配置登录
                .formLogin(form->form

                        // 登录页面
                        .loginPage("/login")

                        // 登录处理
                        .loginProcessingUrl("/dologin")

                        // 登录成功后跳转到首页(用户可自行访问个人中心或管理后台)
                        .defaultSuccessUrl("/")

                        // 登录失败后跳转
                        .failureUrl("/login?error")

                        // 允许所有用户访问
                        .permitAll()
                )

                //配置登出,适用于所有用户
                .logout(logout->logout
                        .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
                        .logoutSuccessUrl("/login?logout")
                        .permitAll()
                )

                //配置CSRF,ignoringRequestMatchers()方法忽略h2-console下的所有请求
                .csrf(csrf->csrf.ignoringRequestMatchers("/h2-console/**"));

        //返回HTTP对象
        return http.build();
    }
}
3.4H2数据库配置

使用CommandLineRunner中的init初始化数据库,添加相关信息

java 复制代码
@Configuration
public class DataInitializer {

    /**
     * 初始化用户
     * @param userMapper 用户映射
     * @param encoder    密码编码器
     * @return 命令行运行器
     */
    @Bean
    CommandLineRunner init(UserMapper userMapper, PasswordEncoder encoder) {
        return args -> {
            if (userMapper.selectCount(null) == 0) {
                // 创建管理员用户
                User admin = new User();
                admin.setUsername("admin");
                admin.setPassword(encoder.encode("123456"));
                admin.setRole("ROLE_ADMIN");
                userMapper.insert(admin);

                // 创建普通用户
                User user = new User();
                user.setUsername("user");
                user.setPassword(encoder.encode("123"));
                user.setRole("ROLE_USER");
                userMapper.insert(user);

                System.out.println(">>> [SpringSecurityTest2] 初始用户已创建");
                System.out.println(">>> 管理员 - 用户名: admin, 密码: 123456");
                System.out.println(">>> 普通用户 - 用户名: user, 密码: 123");
            }
        };
    }
}

同时因为是一次性数据库,需要写入创建数据库脚本,方便每次启动时自动执行,同时也要在yaml文件中打开自动执行功能

mysql 复制代码
-- data.sql

-- 密码 '123' 的 BCrypt 加密结果
INSERT INTO users (username, password, role) VALUES ('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'ROLE_ADMIN');
INSERT INTO users (username, password, role) VALUES ('user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'ROLE_USER');

-- 插入测试文章
INSERT INTO posts (title, content, author) VALUES ('First Post', 'Hello World', 'admin');



-- schema.sql

CREATE TABLE IF NOT EXISTS users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    role VARCHAR(255) NOT NULL
);

CREATE TABLE IF NOT EXISTS posts (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,
    author VARCHAR(255)
);
yaml 复制代码
spring:
  sql:
    init:
      mode: always #始终执行初始数据库的脚本
  datasource:
    url: jdbc:h2:mem:mdblog_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
    password: 123456
  h2:
    console:
      enabled: true
      path: /h2-console

四、 避坑

1.ERR_TOO_MANY_REDIRECTS

Q :配置完成后,访问后台页面,浏览器疯狂跳转,最终报错 ERR_TOO_MANY_REDIRECTS

A:这是一个经典的逻辑死循环:

  • 用户访问 /admin/dashboard -> 未登录 -> Security 拦截 -> 重定向到 /login

  • 用户访问 /login -> 如果 /login 没有被显式放行 -> Security 再次拦截(认为你没登录) -> 再次重定向到 /login

  • 无限循环... 🔄

解决方案

必须在 authorizeHttpRequests 中,显式且准确地放行登录页及相关资源。

java 复制代码
.requestMatchers("/", "/login", "/dologin", "/h2-console/**", "/css/**").permitAll()

注意细节

  1. /dologin (表单提交地址) 也必须放行,否则 POST 请求会被拦截。
  2. /h2-console/** 建议使用双星号通配符,覆盖所有子路径。
  3. 确保 formLogin().loginPage("/login") 中的 URL 与放行列表完全一致。
5. 运行效果展示

启动日志分析

观察控制台日志,可以看到 H2 数据库启动成功,MyBatis-Plus 打印出了初始化的 SQL,且 Security 过滤器链构建完成。

H2数据库成功启动

过滤器链构建完成


功能演示

  1. 访问 http://localhost:8080/admin/dashboard,自动跳转至登录页。
  2. 输入测试账号 admin/user / 123456,登录成功进入后台。
  3. 点击退出,会话清除,再次访问需重新登录。




6. 总结

通过本文,成功整合了 Spring Security 与 MyBatis-Plus。虽然 Security 的配置略显繁琐,但只要理清"过滤器链"和"权限匹配顺序"这两个核心概念,就能轻松驾驭。

MP 的加入更是大大简化了数据层代码,让我们可以更专注于业务逻辑。

同时上传了demo源代码,有兴趣的可以下载运行尝试,可能在登录后会有404问题,建议登录后修改URL为:http://localhost:8080/即可解决问题

下一步计划

后续会融入非遗项目中做进一步的整合与融合

相关推荐
treacle田2 小时前
达梦数据库-配置本地守护进程dmwatcher服务-记录总结
数据库·达梦数据库·达梦数据库local数据守护
wyt5314292 小时前
Redis的安装教程(Windows+Linux)【超详细】
linux·数据库·redis
CeshirenTester2 小时前
从数据库到结构化用例:一套可落地的测试智能体架构
数据库·架构
2301_793804693 小时前
Python数据库操作:SQLAlchemy ORM指南
jvm·数据库·python
不想看见4044 小时前
Qt 项目中实现良好封装(模块化设计)的详细流程指南
数据库·系统架构
mygljx4 小时前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql
没有bug.的程序员4 小时前
Serverless 弹性扩容引发的全线熔断:Spring Boot 启动耗时从 1s 压缩至 0.3s 的物理级绞杀
java·spring boot·kubernetes·serverless·扩容·线上
Jeremy爱编码4 小时前
软考数据库
数据库
Bdygsl5 小时前
MySQL(1)—— 基本概念和操作
数据库·mysql