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

🎏:你只管努力,剩下的交给时间
🏠 :小破站
从0到1掌握 Spring Security(第三篇):三种认证方式,按配置一键切换
- 摘要
- 运行与切换入口 ⚙️
- 原理总览:一次表单登录到底发生了什么?🧠
- 两个关键契约:UserDetailsService 与 PasswordEncoder 📝
- 内存方式的原理(为什么它能工作)🧩
- 数据库方式的原理(为什么它能工作)🗄️
- 条件化装配与“按配置切换”的原理 🔀
- 角色到权限的映射规则(为什么 hasRole 能生效)🛡️
- 认证成功后的“落袋为安”📦
- 三种认证方式与对应账号 👥
- 3) 关键代码位置与作用 🧩
- 4) application.yml 关键片段 📄
- 5) 启动与验证流程 ✅
- 控制台日志对照(便于排错)🖨️
- 常见问题(严格对齐当前实现)🧯
- 小结 🧾
- 感谢
摘要
- 本文严格基于当前仓库代码,讲解并落地三种认证方式:配置文件、内存、数据库;
- 通过
acowbo.auth.type
在 config | memory | database 之间切换,无需改代码; - 文中所有账户、路径、端口与日志均与当前实现一致,避免歧义。
运行与切换入口 ⚙️
- 端口:18080
- 切换认证方式(application.yml)
acowbo:auth:type: config # config | memory | database
启动后,控制台会输出所用认证方式的加载日志。
原理总览:一次表单登录到底发生了什么?🧠
-
UsernamePasswordAuthenticationFilter.attemptAuthentication
- 从请求参数读取用户名/密码(可自定义参数名)
- 构造未认证的 UsernamePasswordAuthenticationToken
- principal = 用户名(String)
- credentials = 明文密码(String)
- authenticated = false
- 调用 AuthenticationManager.authenticate(token)
-
ProviderManager.authenticate
- 依次遍历持有的 AuthenticationProvider(按注册顺序)
- 根据 supports(token.getClass()) 选择能处理该 token 的 Provider
- 表单登录场景通常命中 DaoAuthenticationProvider(它继承自 AbstractUserDetailsAuthenticationProvider)
- 调用 provider.authenticate(token)
-
AbstractUserDetailsAuthenticationProvider.authenticate(模板方法,DaoAuthenticationProvider 走这里)
- 参数 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)
- 密码校验扩展点(DaoAuthenticationProvider 实现):
- postAuthenticationChecks
- 可选的凭证过期等检查
- 成功则 createSuccessAuthentication(…)
- 构造“已认证”的 UsernamePasswordAuthenticationToken:
- principal = UserDetails
- credentials 通常被擦除/置空
- authorities = ROLE_XXX 集合
- authenticated = true
- 构造“已认证”的 UsernamePasswordAuthenticationToken:
- retrieveUser(username, authentication)
-
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,并给出一个 PasswordEncoder,DaoAuthenticationProvider 就能完成认证。这正是“基于内存/数据库/配置文件”的核心共性:
- 差异只在于 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
- 三个小点:
- 用户实体与表结构(users):包含 username、encoded password、roles、状态位等
- 数据访问(UserRepository):提供 findByUsername(username)
- 适配器(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(按配置构造用户)
-
内存(memory)
- 账号:memuser/mem123(USER)、memadmin/memadmin123(USER,ADMIN)、memmanager/memmanager123(USER,MANAGER)
- 源:应用启动时写入内存 Map
- 启用:acowbo.auth.type=memory
- 关键类:InMemoryUserDetailsService(初始化与加载)
-
数据库(database)
- 账号:dbuser/db123(USER)、dbadmin/dbadmin123(USER,ADMIN)、dbmanager/dbmanager123(USER,MANAGER)
- 源:H2 内存库 users 表(启动时初始化)
- 启用:acowbo.auth.type=database
- 关键类:DatabaseUserDetailsService、CustomUserDetails、UserRepository、DataInitializer
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 同源。
- SecurityConfig:
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) 启动与验证流程 ✅
- 设置认证方式(示例:config)
- acowbo.auth.type=config
- 启动后控制台将出现:🔧 [配置文件认证] ConfigUserDetailsService 已启用
- 登录
- 浏览器访问:/ → /dashboard(受保护)会引导到 /login
- 用对应方式的账号登录:
- config:configuser/config123
- memory:memuser/mem123 或 memadmin/memadmin123
- database:dbuser/db123 或 dbadmin/dbadmin123
- 验证授权
- 使用管理员账号访问 /admin
- memory:memadmin
- database:dbadmin
- 数据库模式额外验证(可选)
- 打开 /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,生活多彩,心情常青!🚀