Day 21:03. 基于 SpringBoot4 开发后台管理系统-整合 SpringSecurity 完成登录功能

一、前言

回归主线,接下来我们要完成登录的功能,在开始前,我希望你能抛弃之前对后台管理系统的刻板印象,比如RBAC 权限模型,tokenJWT 还是 UUID?存 Cookie 还是 Session?把这些通通抛之脑后,以具体的业务驱动,当我们真的需要使用到 jwt 或者 RBAC 的时候再将它们重拾起来。

二、登录逻辑梳理

为什么要登录,可不可以不要登录?

当然可以,我知道你肯定认为我在抬杠,听我解释。

首先前提是,服务端里有一份名单,这份名单里面有着系统用户的信息,【用户名】 + 【密码】

本质上只要我在请求受限制的资源时,告诉你我是谁,是不是就可以了。

也就是说,每次我请求的时候,都带上 【用户名】 + 【密码】

服务端在处理我的请求的时候,在名单上对照着查,找到了,证明我是合法用户,就该给我返回正确的结果。

抛开其它考虑,这样没有任何问题。

那把抛开的捡回来,为什么不这么做:

  • 每次请求你让用户输入用户名密码,那用户不知道跑哪去了
  • 聪明的你想到把用户名密码保存在客户端,直接自动带过去,好的,盗号风险直接拉满了。

所以很自然的演变成了这种方式,在正式访问系统资源时先登录,使用【用户名】 + 【密码】登录,服务端验证合法后,返回给客户端一个密钥,也就是 token ,所以在名单上,我的信息变成了 【用户名】 + 【密码】+ 【token】, 以后我只要带上 token 就可以了。

通过登录引入 token,解决了频繁输入用户名密码的问题。

到这里其实就已经很清楚了,登录,就是用 【用户名】 + 【密码】,换一把钥匙。无论你怎么做,最终都是围绕着这件事展开。

那我们开始编码了,其实我原本是想用 Sa-Token 的,但是发现 SpringBoot4 不适配 , 所以还是使用了 Spring Security ,官方指定无需考虑兼容性问题,下面参考一下 Sa-Token 的登录认证流程图。

三、整合 Spring Security

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

当我们引入这个依赖后,整合就已经完成了。

启动项目,可以看到控制台输出

我们写一个接口测试一下

java 复制代码
@RestController
@RequestMapping("/system/auth")
public class AuthController {

    @GetMapping("/hello")
    public String hello() {
        return "hello world";
    }

}

页面跳转到了登录界面。

我们输入控制台的密码,默认用户名为 user,登录成功后,请求正常

并且我们发现,通过 Cookie 往服务端传了个 JSESSIONID

是不是很熟悉这套操作,拿 【用户名】 + 【密码】换来了 JSESSIONID

至于 Spring Security 背后帮我们做了些什么其它操作(比如这个登录表单哪来的?),我们暂且不深究,但是我们知道,他本质上其实就是完成了这么个流程。

当然这个只是他的默认机制,接下来我们只需要遵循框架要求,完成我们自己的定制化修改就好了。

接下来我们来完成这个改造工作。

四、自定义登录流程

因为我这不是 SpringSecurity 的教程,我不在这里大篇幅的介绍它。如果一点都不了解的,我建议先去学习一下。官网地址 docs.spring.io/spring-boot...

拿【用户名】 + 【密码】换【Token】 时刻记住这句话。

4.1. 关闭默认的安全配置

我们只需要自己添加一个类型为 SecurityFilterChainbean 即可。

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

    /**
     * 最小化的安全过滤链配置
     * 完全允许所有请求,不进行任何认证和授权
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 完全禁用所有安全功能
                .authorizeHttpRequests(authorize -> authorize
                        // 允许所有请求访问
                        .anyRequest().permitAll()
                )
                // 禁用CSRF保护
                .csrf(csrf -> csrf.disable())
                // 禁用表单登录
                .formLogin(form -> form.disable())
                // 禁用HTTP基本认证
                .httpBasic(basic -> basic.disable())
                // 禁用会话管理
                .sessionManagement(session -> session.disable());

        return http.build();
    }

    /**
     * 配置一个空的UserDetailsService,关闭默认的UserDetailsService配置
     * 这样就完全禁用了Spring Security的默认安全功能
     */
    @Bean
    public UserDetailsService userDetailsService() {
        // 返回一个空的InMemoryUserDetailsManager,不包含任何用户
        return new InMemoryUserDetailsManager();
    }
}

重启后发现,如我们所愿,似乎一切都没有发生过一样

现在我们就要开始完成我们自己的逻辑了,如果没有用框架,我们会怎么实现

首先通过请求传参给服务端,服务端拿到参数根据用户名查询数据库,对比密码,如果正确,创建个 tokentoken 与用户关联,然后将 token 返回给前端。

现在借助框架,我们如何把上面这些步骤安排给它来帮我们完成?

4.2. 自定义配置

默认情况下是通过表单来接收参数的,框架帮我们实现了一个 /login 请求,走的框架自带的认证流程。

接下来我们要顺着我们自己的思路来,通过 controller 接收请求,主动去使用 SpringSecurity 帮我们验证。简单来说就是把接收参数前面这段掐断,从自动从过滤器中获取参数,改成我们自己来获取。

这里如果细说,文章就太啰嗦了,我列出关键点,完整代码大家可以去代码仓库查看。

4.2.1 创建用户

这其实就是服务端要现有一个名单,创建用户就是往名单上加信息。

java 复制代码
@PostMapping("/init")
public Result<Void> initUser() {
    userService.initUser();
    return Result.success("初始化用户成功 用户名:admin 密码:123456 ");
}

4.2.2 自定义用户安全信息对象 CustomUserDetails

这里有的人喜欢直接利用数据层对象,在 User 上直接去实现 UserDetails ,这种做法在以前的项目中很常见,但是,我想说的是,不合理。

  • 破坏了关注点分离原则

数据实体(Entity)的核心职责在于业务领域建模和数据持久化 ,而 UserDetails接口是安全框架的认证契约。将两者耦合,意味着同一个类需要同时承担数据映射与安全适配两种截然不同的职责,这显著降低了代码的内聚性和可维护性。

  • 造成了不必要的框架耦合与侵蚀

数据层应是相对稳定、独立的基础设施。直接实现 UserDetails会将数据实体与 Spring Security 这一具体的安全实现框架强绑定,使得领域模型被技术实现细节所"污染"。这导致未来更换、升级或移除安全框架时,重构成本高昂,并严重损害了核心业务模型的可移植性。

正确的做法

java 复制代码
// 安全层对象  UserDetails实现类、用户详情对象、安全用户对象
public class CustomUserDetails implements UserDetails { 
	private final User user;  // 组合而非继承
   ...
}  

4.2.3 自定义 UserDetailsService 实现 loadUserByUsername 方法

这个是服务端进行对照的关键步骤,等价于,从名单上找出符合的用户信息。注意这里只需要将名单上的用户信息组装成 **安全用户对象 ** CustomUserDetails 即可,具体的密码校验逻辑,框架会帮我们完成。具体细节可以看 DaoAuthenticationProvider --> retrieveUser 方法。

java 复制代码
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public @NullMarked UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查找用户,找不到则抛出异常
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
        // 返回的 UserDetails 对象包含了用户的密码和权限信息,Spring Security 会用它进行认证
        CustomUserDetails customUserDetails = new CustomUserDetails();
        customUserDetails.setUser(user);
        return customUserDetails;
    }
}

4.2.4 自定义 Spring Security 配置类

事实上我们只需要自定义好 CustomUserDetailsService 就已经完成了基本配置,剩下的只需要根据我们的需要来进行配置过滤器链就可以了。

具体的默认配置情况,参考源码:InitializeUserDetailsBeanManagerConfigurer 这里交代的很清楚。

但是你会发现,我的配置类中加了这么一段

java 复制代码
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
    // 从配置中获取默认的AuthenticationManager
    // 如果配置了AuthenticationProvider,它会自动使用这些Provider
    return authConfig.getAuthenticationManager();
}

当我在刚学习 SpringSecurity 时,看到很多参考项目,发现有的这样配置,有的那样配置,只知道这么配置就可以,但是一直不明白为什么?

比如这里,前面已经提到了,默认情况下,明明已经提供了一个 AuthenticationManager

为什么还要在这里显示配置 AuthenticationManager ?

直到今天,我再回过头来看的时候发现, Spring Boot 默认情况下 不会AuthenticationManager作为 Bean暴露在 Spring 容器中。可以看到这里并没有 @Bean 注解。

java 复制代码
// 源码地址 InitializeUserDetailsBeanManagerConfigurer
public AuthenticationManager getAuthenticationManager() {
    if (this.authenticationManagerInitialized) {
        return this.authenticationManager;
    }
    ...
}

通过初始化的源码发现:AuthenticationConfigurationInitializeUserDetailsBeanManagerConfigurer

当项目中存在UserDetailsService实现(目中的CustomUserDetailsService)时,Spring Security 会自动:

  • 创建DaoAuthenticationProvider实例
  • 将你的UserDetailsServicePasswordEncoder注入到该 Provider
  • 构建一个AuthenticationManager实例来协调这些 Provider

需要显示配置是因为:在项目中,AuthService类直接依赖并注入了AuthenticationManager

java 复制代码
@Service
@RequiredArgsConstructor
public class AuthService {
    private final AuthenticationManager authenticationManager; // 直接注入
    
    public String login(LoginDTO loginDTO) {
        // 使用authenticationManager进行认证
        Authentication authentication = authenticationManager.authenticate(authToken);
        // ...
    }
}

所以要想使用,得自己手动暴露给 Spring 容器,显式配置了一个 AuthenticationManagerBean

总结:

  • getAuthenticationManager()方法本身没有@Bean注解,不会自动暴露
  • 需要通过手动定义@Bean方法的方式将其返回的实例注册为 Spring Bean
  • 这种设计提供了更大的灵活性,允许用户根据需要自定义AuthenticationManager的暴露方式

配置类:

java 复制代码
/**
 * Spring Security 配置类
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级别的安全注解
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * AuthenticationManager 说明:
     * AuthenticationManager是Spring Security认证体系的入口点
     * 它协调多个AuthenticationProvider实例,尝试对认证请求进行处理
     * 如果一个AuthenticationProvider无法认证,它会尝试下一个
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        // 从配置中获取默认的AuthenticationManager
        // 如果配置了AuthenticationProvider,它会自动使用这些Provider
        return authConfig.getAuthenticationManager();
    }

    /**
     * 完全允许所有请求,不进行任何认证和授权
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        ...
            return http.build();
    }


}

看到这里,我不知道你是否有这样的疑问,为什么默认情况不暴露,也可以实现登录(默认表单情况下),我们是不是也可以不用暴露做。

完全可以,我之前的项目中就是通过自定义过滤器链,来进行完全的配置化,但是那么做的结果就是,不够清晰,因为接口是隐藏式的,不利于接口的统一管理。

如果你想要尝试,可以你看一下这个 AbstractAuthenticationProcessingFilter ,参考 UsernamePasswordAuthenticationFilter 实现一个我们自己的过滤器,比如 JSONUsernamePasswordAuthenticationFilter

五、总结

原本想要将鉴权部分一起写完的,但是这个就有点 已知结果、按图索骥的感觉 ,又成了 "预制版", 所以就此打住,我们目前做的就是拿 【用户名】 + 【密码】换【Token】

千里之行,始于足下。你的"个人公司"从这第一个2小时开始。欢迎在评论区分享你的进展或遇到的卡点,我会逐一查看,尽可能的帮助解决。我们下一篇文章见!

相关推荐
小徐不会敲代码~2 分钟前
Vue3 学习 5
前端·学习·vue
_Kayo_3 分钟前
vue3 状态管理器 pinia 用法笔记1
前端·javascript·vue.js
How_doyou_do3 分钟前
工程级前端智能体FrontAgent
前端
superman超哥3 分钟前
Rust impl 块的组织方式:模块化设计的艺术
开发语言·后端·rust·模块化设计·rust impl块·impl块
superman超哥9 分钟前
仓颉跨语言编程:FFI外部函数接口的原理与深度实践
开发语言·后端·仓颉编程语言·仓颉·仓颉语言·仓颉跨语言编程·ffi外部函数接口
咕白m6259 分钟前
通过 Python 提取 PDF 表格数据(导出为 TXT、Excel 格式)
后端·python
2501_9444460011 分钟前
Flutter&OpenHarmony日期时间选择器实现
前端·javascript·flutter
二狗哈12 分钟前
Cesium快速入门34:3dTile高级样式设置
前端·javascript·算法·3d·webgl·cesium·地图可视化
JS_GGbond13 分钟前
前端实战:让表格Header优雅吸顶的魔法
前端
AlanHou14 分钟前
Three.js:Web 最重要的 3D 渲染引擎的技术综述
前端·webgl·three.js