当前位置: 首页 > news >正文

从0到1掌握 Spring Security(第三篇):三种认证方式,按配置一键切换

欢迎来到我的博客,代码的世界里,每一行都是一个故事


在这里插入图片描述

🎏:你只管努力,剩下的交给时间

🏠 :小破站

从0到1掌握 Spring Security(第三篇):三种认证方式,按配置一键切换

      • 摘要
    • 运行与切换入口 ⚙️
    • 原理总览:一次表单登录到底发生了什么?🧠
    • 两个关键契约:UserDetailsService 与 PasswordEncoder 📝
    • 内存方式的原理(为什么它能工作)🧩
    • 数据库方式的原理(为什么它能工作)🗄️
    • 条件化装配与“按配置切换”的原理 🔀
    • 角色到权限的映射规则(为什么 hasRole 能生效)🛡️
    • 认证成功后的“落袋为安”📦
    • 三种认证方式与对应账号 👥
    • 3) 关键代码位置与作用 🧩
    • 4) application.yml 关键片段 📄
    • 5) 启动与验证流程 ✅
    • 控制台日志对照(便于排错)🖨️
    • 常见问题(严格对齐当前实现)🧯
    • 小结 🧾
    • 感谢

摘要

  • 本文严格基于当前仓库代码,讲解并落地三种认证方式:配置文件内存数据库
  • 通过 acowbo.auth.typeconfig | memory | database 之间切换,无需改代码
  • 文中所有账户、路径、端口与日志均与当前实现一致,避免歧义。

运行与切换入口 ⚙️

  • 端口:18080
  • 切换认证方式(application.yml)
acowbo:auth:type: config  # config | memory | database

启动后,控制台会输出所用认证方式的加载日志。


原理总览:一次表单登录到底发生了什么?🧠

  • UsernamePasswordAuthenticationFilter.attemptAuthentication

    • 从请求参数读取用户名/密码(可自定义参数名)
    • 构造未认证的 UsernamePasswordAuthenticationToken
      • principal = 用户名(String)
      • credentials = 明文密码(String)
      • authenticated = false
    • 调用 AuthenticationManager.authenticate(token)

    image-20250813170246589

  • ProviderManager.authenticate

    • 依次遍历持有的 AuthenticationProvider(按注册顺序)
    • 根据 supports(token.getClass()) 选择能处理该 token 的 Provider
    • 表单登录场景通常命中 DaoAuthenticationProvider(它继承自 AbstractUserDetailsAuthenticationProvider)
    • 调用 provider.authenticate(token)

    image-20250813205706310

  • AbstractUserDetailsAuthenticationProvider.authenticate(模板方法,DaoAuthenticationProvider 走这里)

    image-20250813205946547

    • 参数 authentication 就是页面表单生成的 UsernamePasswordAuthenticationToken(含用户名与明文密码)
    • 核心步骤:
      • retrieveUser(username, authentication)
        • DaoAuthenticationProvider 实现:调用 UserDetailsService.loadUserByUsername(username)
        • 返回 UserDetails(username、encodedPassword、authorities、enabled/locked/expired 等)
      • preAuthenticationChecks
        • 账号启用/未锁定/未过期等状态检查,不通过抛异常
      • additionalAuthenticationChecks(user, authentication)
        • 密码校验扩展点(DaoAuthenticationProvider 实现):
          • raw = authentication.getCredentials()
          • encoded = user.getPassword()
          • PasswordEncoder.matches(raw, encoded) 校验
          • 不匹配抛 BadCredentialsException(通常隐藏 UsernameNotFound)
      • postAuthenticationChecks
        • 可选的凭证过期等检查
      • 成功则 createSuccessAuthentication(…)
        • 构造“已认证”的 UsernamePasswordAuthenticationToken:
          • principal = UserDetails
          • credentials 通常被擦除/置空
          • authorities = ROLE_XXX 集合
          • authenticated = true
  • AbstractAuthenticationProcessingFilter.successfulAuthentication(过滤器收尾)

    • SecurityContextHolder.getContext().setAuthentication(auth)
    • 触发 AuthenticationSuccessHandler(默认重定向到原目标)
  • SecurityContextPersistenceFilter(请求边界)

    • 请求开始:从 Session 恢复 SecurityContext
    • 请求结束:将 SecurityContext 持久化回 Session

要点

  • UserDetailsService 决定“用户从哪儿来”(内存 Map、数据库、配置文件)

  • PasswordEncoder 决定“如何比对密码”(raw vs encoded)

  • DaoAuthenticationProvider 在 additionalAuthenticationChecks 中完成密码校验

  • ProviderManager 通过 supports 精确路由到合适的 Provider

  • 后续授权由 FilterSecurityInterceptor 基于 Authorities 决策(如 hasRole(“ADMIN”))

结论:只要你提供了一个能“按用户名返回用户详情”的 UserDetailsService,并给出一个 PasswordEncoderDaoAuthenticationProvider 就能完成认证。这正是“基于内存/数据库/配置文件”的核心共性:

  • 差异只在于 UserDetailsService 从哪儿拿用户(Map、DB、yml)

两个关键契约:UserDetailsService 与 PasswordEncoder 📝

  • UserDetailsService

    • 契约:loadUserByUsername(String username)
    • 返回:UserDetails(至少包含 username、encoded password、Authorities,以及账户状态)
    • 你的实现决定“到哪里查用户”:内存 Map、数据库、配置文件
  • PasswordEncoder

    • 契约:encode(raw) 与 matches(raw, encoded)
    • 作用:把明文密码安全地与存储的编码密码比对(推荐 BCrypt)

二者一配齐,DaoAuthenticationProvider 才能完成认证流程。


内存方式的原理(为什么它能工作)🧩

  • 思路:用一个 Map<String, UserDetails> 做为“用户库”,按用户名从 Map 里“取出用户”,就是 loadUserByUsername 的答案
  • 角色与权限:角色(ROLE_XXX)本质也是 GrantedAuthority,在构造 UserDetails 时以 ROLE_ 前缀填充 Authorities
  • 密码:启动时把明文通过 PasswordEncoder.encode 存入 Map,匹配时走 matches
  • 优点:实现最简单,零外部依赖;缺点:进程内存,重启即失

对应到项目:InMemoryUserDetailsService 在构造器里初始化若干用户,loadUserByUsername 直接从 Map 中返回 UserDetails。


数据库方式的原理(为什么它能工作)🗄️

  • 思路:把“内存 Map”换成“数据库表”,按用户名从表里查一行记录,把这行记录适配为 UserDetails
  • 三个小点:
    1. 用户实体与表结构(users):包含 username、encoded password、roles、状态位等
    2. 数据访问(UserRepository):提供 findByUsername(username)
    3. 适配器(CustomUserDetails):把实体转换为 UserDetails,并把字符串 roles(如 “USER,ADMIN”)映射为 Authorities(ROLE_USER, ROLE_ADMIN)
  • 认证时序:DaoAuthenticationProvider → userRepository.findByUsername → new CustomUserDetails(entity) → PasswordEncoder.matches → 通过则返回已认证 Authentication
  • 优点:持久化、可扩展;缺点:需要 DB 与 ORM 配置

对应到项目:DatabaseUserDetailsService 负责查库并返回 CustomUserDetails;DataInitializer 在 database 模式启动时写入三类测试账号。


条件化装配与“按配置切换”的原理 🔀

  • 我们给三种来源分别做了 @ConditionalOnProperty
    • config → 启用 ConfigUserDetailsService
    • memory → 启用 InMemoryUserDetailsService
    • database → 启用 DatabaseUserDetailsService
  • 同一时间只有一个 UserDetailsService Bean 存在,ProviderManager 在认证时就只会调用到那一个来源,避免歧义与冲突
  • 切换 = 改 yml + 重启;其余认证链条(过滤器、Provider)保持不变

角色到权限的映射规则(为什么 hasRole 能生效)🛡️

  • Spring Security 在 URL/方法授权时对 hasRole(“ADMIN”) 的处理,等价于校验用户是否拥有 “ROLE_ADMIN” 这个 GrantedAuthority
  • 因此在内存与数据库两种来源里,我们都把业务角色字符串(如 “USER,ADMIN”)转成权限集合:ROLE_USER、ROLE_ADMIN
  • 这样 FilterSecurityInterceptor 基于 Authorities 做决策时才能命中规则

认证成功后的“落袋为安”📦

  • DaoAuthenticationProvider 认证通过后会返回“已认证”的 Authentication,其中包含:Principal(UserDetails)+ Authorities
  • SecurityContextHolder 保存该 Authentication;请求结束时由 SecurityContextPersistenceFilter 写回 Session
  • 下次请求开始时再由同一过滤器恢复上下文,因此你在 Controller/模板里能直接拿到当前用户与权限

三种认证方式与对应账号 👥

为避免混淆,每种方式使用不同的前缀用户名(与代码保持一致)。

  • 配置文件(config)

    • 账号:configuser/config123
    • 源:spring.security.user.*(yml)
    • 启用:acowbo.auth.type=config
    • 关键类:ConfigUserDetailsService(按配置构造用户)

    image-20250813155317362

    image-20250813155356453

  • 内存(memory)

    • 账号:memuser/mem123(USER)、memadmin/memadmin123(USER,ADMIN)、memmanager/memmanager123(USER,MANAGER)
    • 源:应用启动时写入内存 Map
    • 启用:acowbo.auth.type=memory
    • 关键类:InMemoryUserDetailsService(初始化与加载)

    image-20250813155508475

    image-20250813155548386

  • 数据库(database)

    • 账号:dbuser/db123(USER)、dbadmin/dbadmin123(USER,ADMIN)、dbmanager/dbmanager123(USER,MANAGER)
    • 源:H2 内存库 users 表(启动时初始化)
    • 启用:acowbo.auth.type=database
    • 关键类:DatabaseUserDetailsService、CustomUserDetails、UserRepository、DataInitializer

    image-20250813155650077

    image-20250813155744776


3) 关键代码位置与作用 🧩

  • 账号加载

    • 配置:ConfigUserDetailsService(读取 spring.security.user.*,必要时对明文密码做 BCrypt 编码)
    • 内存:InMemoryUserDetailsService(构造并缓存 UserDetails)
    • 数据库:DatabaseUserDetailsService(通过 UserRepository 读取,包装为 CustomUserDetails)
  • 实体与仓库(database)

    • 实体:com.acowbo.entity.User(Lombok + JPA,表名 users)
    • 仓库:UserRepository(findByUsername)
    • 自定义 UserDetails:CustomUserDetails(将 roles 映射为权限集合)
    • 数据初始化:DataInitializer(仅 database 模式创建 dbuser/dbadmin/dbmanager)
  • 安全配置(统一)

    • SecurityConfig
      • 自定义登录页 /login;
      • 放行 /、/login、静态资源、/h2-console/**;
      • /admin/** 需 ADMIN 角色;
      • CSRF 对 H2 控制台放行,frameOptions 同源。

4) application.yml 关键片段 📄

  • 端口与模板引擎
server:port: 18080spring:thymeleaf:cache: falseencoding: UTF-8mode: HTMLprefix: classpath:/templates/suffix: .html
  • 配置文件模式账户(config 模式使用)
spring:security:user:name: configuserpassword: config123roles: USER
  • 数据库/H2 控制台(database 模式使用)
spring:datasource:url: jdbc:h2:mem:testdbdriver-class-name: org.h2.Driverusername: sapassword:jpa:hibernate:ddl-auto: create-dropshow-sql: trueh2:console:enabled: truepath: /h2-console
  • 切换认证方式
acowbo:auth:type: config  # config | memory | database

5) 启动与验证流程 ✅

  1. 设置认证方式(示例:config)
  • acowbo.auth.type=config
  • 启动后控制台将出现:🔧 [配置文件认证] ConfigUserDetailsService 已启用
  1. 登录
  • 浏览器访问:/ → /dashboard(受保护)会引导到 /login
  • 用对应方式的账号登录:
    • config:configuser/config123
    • memory:memuser/mem123 或 memadmin/memadmin123
    • database:dbuser/db123 或 dbadmin/dbadmin123
  1. 验证授权
  • 使用管理员账号访问 /admin
    • memory:memadmin
    • database:dbadmin
  1. 数据库模式额外验证(可选)
  • 打开 /h2-console(JDBC: jdbc:h2:mem:testdb,用户 sa,密码空)
  • 查看 users 表,确认初始化账号存在

控制台日志对照(便于排错)🖨️

  • 成功加载用户

    • ✅ [配置文件认证] 成功加载用户: configuser (角色: USER)
    • ✅ [内存认证] 成功加载用户: memadmin (角色: [ROLE_USER, ROLE_ADMIN])
    • ✅ [数据库认证] 成功加载用户: dbuser (角色: USER)
  • 用户不存在/用户名不匹配

    • ❌ [配置文件认证] 用户不存在: xxx (期望用户名: configuser)
    • ❌ [内存认证] 用户不存在: xxx (可用用户: [memuser, memadmin, memmanager])
    • ❌ [数据库认证] 用户不存在: xxx

常见问题(严格对齐当前实现)🧯

  • 登录失败:确认 acowbo.auth.type 与所用账号前缀一致(config/memory/database)
  • /admin 403:需 ADMIN 角色(memadmin 或 dbadmin)
  • H2 控制台打不开:确认访问 /h2-console,且当前配置使用了 H2;默认连接信息见上一节

小结 🧾

  • 你现在已具备:三种认证来源 + 配置化切换 + 角色/授权演练
  • 文档与代码一一对应,账号、日志、路径、端口均以仓库当前实现为准
  • 后续可将 H2 切换至 MySQL/PostgreSQL,或引入 OAuth2/JWT 等更贴近生产的方案。

感谢

感谢你读到这里,说明你已经成功地忍受了我的文字考验!🎉
希望这篇文章没有让你想砸电脑,也没有让你打瞌睡。
如果有一点点收获,那我就心满意足了。

未来的路还长,愿你
遇见难题不慌张,遇见bug不抓狂,遇见好内容常回访
记得给自己多一点耐心,多一点幽默感,毕竟生活已经够严肃了。

如果你有想法、吐槽或者想一起讨论的,欢迎留言,咱们一起玩转技术,笑对人生!😄

祝你代码无bug,生活多彩,心情常青!🚀
在这里插入图片描述

http://www.dtcms.com/a/334245.html

相关文章:

  • Flink Stream API 源码走读 - print()
  • TDengine IDMP 高级功能(3. 概念解释)
  • 用Pygame开发桌面小游戏:从入门到发布
  • MixOne:Electron Remote模块的现代化继任者
  • AI 云电竞游戏盒子:从“盒子”到“云-端-芯”一体化竞技平台的架构实践
  • 【BFS 重构树】P11907 [NHSPC 2023] F. 恐怖的黑色魔物|省选-
  • AI的下一个竞争焦点——世界模型
  • 笔试——Day40
  • 超酷炫的Three.js示例
  • Proteus 入门教程
  • 深度剖析setjmp/longjmp:非局部跳转的内部机制与协程应用限制
  • 双重调度(Double Dispatch):《More Effective C++》条款31
  • RD-Agent for Quantitative Finance (RD-Agent(Q))
  • C#单元测试(xUnit + Moq + coverlet.collector)
  • 深度学习——常见问题与优化改进
  • java中消息推送功能
  • Xiaothink-T6-0.15B混合架构模型深度解析
  • 3 种方式玩转网络继电器!W55MH32 实现网页 + 阿里云 + 本地控制互通
  • 架构调整决策
  • 超越Transformer:大模型架构创新的深度探索
  • 【计算机网络架构】混合型架构简介
  • Blackwell 和 Hopper 架构的 GPGPU 新功能全面综述
  • 【LeetCode每日一题】
  • Mac (三)如何设置环境变量
  • 从希格斯玻色子到 QPU:C++ 的跨维度征服
  • 代码随想录Day52:图论(孤岛的总面积、沉没孤岛、水流问题、建造最大岛屿)
  • 在ubuntu系统上离线安装jenkins的做法
  • 立体匹配中的稠密匹配和稀疏匹配
  • 8.16 pq
  • [系统架构设计师]系统质量属性与架构评估(八)