Spring Security 用户认证配置深度解析
一、核心配置方案对比
1. UserDetailsService Bean 定义方式(推荐)
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(
User.withUsername("admin")
.password("{noop}admin123") // {noop}表示明文密码
.roles("USER", "ADMIN")
.build()
);
return manager;
}
优势:
声明式配置,符合 Spring 惯用模式
自动注册到 Spring 容器,可被其他组件注入
与自动配置完美兼容
执行时机:在应用启动时由 Spring 容器初始化
2. AuthenticationManagerBuilder 注入方式
@Autowired
public void initialize(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService());
}
注意点:
此方法会覆盖默认配置
需要手动关联已定义的 UserDetailsService
执行顺序早于 configure() 方法
二、配置优先级与覆盖关系
执行流程图示
graph TD
A[自动配置默认用户] -->|被覆盖| B[initialize方法配置]
B -->|被覆盖| C[configure方法配置]
C -->|被覆盖| D[显式定义的UserDetailsService Bean]
覆盖规则说明
UserDetailsService Bean 定义具有最高优先级
configure() 方法会覆盖 initialize() 的配置
YAML 配置 (spring.security.user) 优先级最低
三、生产环境最佳实践
1. 多用户内存配置
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password("{noop}user123")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$...")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
2. 数据库存储方案
@Bean
public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
manager.setUsersByUsernameQuery("SELECT username, password, enabled FROM users WHERE username=?");
manager.setAuthoritiesByUsernameQuery("SELECT username, authority FROM authorities WHERE username=?");
return manager;
}
3. 密码编码配置
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
四、关键问题解答
Q1:为什么定义 UserDetailsService Bean 后不需要手动调用 userDetailsService()?
因为 Spring Security 会自动检测容器中的 UserDetailsService 实现,并自动将其配置到认证流程中。
Q2:何时需要使用 AuthenticationManagerBuilder?
当需要:
配置多个认证源(如内存+JDBC)
自定义认证提供者链
配置特殊的 AuthenticationProvider
Q3:密码中的 {noop} 前缀作用?
这是 Spring Security 的密码编码标识:
{noop}:明文密码
{bcrypt}:BCrypt 哈希
{pbkdf2}:PBKDF2 哈希
五、调试技巧
查看生效的配置类:
在 application.properties 添加: debug=true
验证用户服务:
@Autowired
private UserDetailsService userDetailsService;
@GetMapping("/test-user")
public String testUser() {
UserDetails user = userDetailsService.loadUserByUsername("admin");
return user.getAuthorities().toString();
}
监控认证事件:
@EventListener
public void onAuthSuccess(AuthenticationSuccessEvent event) {
log.info("用户 {} 认证成功", event.getAuthentication().getName());
}
六、版本兼容说明
Spring Boot 版本
特性变化
2.4.x 及之前
WebSecurityConfigurerAdapter 为主要配置方式
2.5.x-2.7.x
开始支持组件化配置
3.0.x+
强制组件化配置,移除 WebSecurityConfigurerAdapter
建议新项目直接使用组件化配置方式。
七、
// 自定义 AuthenticationManager 后可以在任何位置进行注入, 否则会报错
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
自定义 自定义 AuthenticationManager 后 就可以 在任何位置注入使用了 @Autowired private AuthenticationManager authManager; // 按类型匹配
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/* // 这个 方法 是为了 自定义 UserDetailsService, 并将其 注册到 工厂中,
// 以便 后续 自定义 登录逻辑时 可以 从 工厂中 取出 这个 实例
// 这里自定义了 UserDetailsService 后续操作就会检测发现有自定义的就 自动加载这个UserDetailsService 的 bean 实例,
// 这样 下边的 @Autowired initialize 就可以 从 工厂中 取出 这个 实例, 其实下边的 builder.userDetailsService(userDetailsService()); 就是多余的了,
// 因为 自定义的UserDetailsService 已经主测 到工厂中了 builder.userDetailsService 只是把已经定义好的在设置一遍而已
如果 使用 自定义 AuthenticationManager 的 public void configure(AuthenticationManagerBuilder builder) 方法 的话 那他会把默认的 AuthenticationManager 配置覆盖掉,
那就得在自定义的 方法 configure 里 加上 builder.userDetailsService(userDetailsService()); 否则不生效
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetails = new InMemoryUserDetailsManager();
userDetails.createUser(User.withUsername("aaa").password("{noop}111").roles("USER").build());
return userDetails;
}
// springboot 对 security 的默认配置中 在工厂中默认创建 一个 AuthenticationManager 实例
@Autowired
public void initialize(AuthenticationManagerBuilder builder) throws Exception {
// System.out.println("springboot 默认配置 -》 初始化 AuthenticationManager : " + builder);
// InMemoryUserDetailsManager userDetails = new InMemoryUserDetailsManager();
// userDetails.createUser(User.withUsername("aaa").password("{noop}111").roles("USER").build());
// builder.userDetailsService(userDetails);
// builder.userDetailsService(userDetailsService());
}
// 自定义 AuthenticationManager, 自定义的这个会覆盖 “initialize(AuthenticationManagerBuilder builder)” 的配置
// @Override
// public void configure(AuthenticationManagerBuilder builder){
// System.out.println("自定义 AuthenticationManager : " + builder);
// }
// 自定义 AuthenticationManager 后可以在任何位置进行注入, 否则会报错
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 定义需要忽略的无效路径前缀
private static final List<String> IGNORED_PATHS = Arrays.asList(
"/.well-known/", // 屏蔽 Chrome 调试工具路径
"/css/", "/js/", "/img/", // 屏蔽静态资源
"/favicon.ico", // 屏蔽浏览器图标请求
"/error" // 屏蔽错误页面请求
);
@Bean
public RequestCache requestCache() {
return new HttpSessionRequestCache() {
@Override
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
// 仅当请求路径不是无效路径时,才保存为 SavedRequest
String requestUri = request.getRequestURI();
boolean isInvalidPath = IGNORED_PATHS.stream().anyMatch(requestUri::startsWith);
if (!isInvalidPath) {
super.saveRequest(request, response);
}
// 无效路径不保存,自然不会被作为跳转目标
}
@Override
public SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response) {
// 可选:获取时也过滤,双重保障
SavedRequest savedRequest = super.getRequest(request, response);
if (savedRequest != null) {
String redirectUrl = savedRequest.getRedirectUrl();
boolean isInvalid = IGNORED_PATHS.stream().anyMatch(redirectUrl::contains);
if (isInvalid) {
return null; // 无效路径返回 null,使用默认跳转
}
}
return savedRequest;
}
};
}
/**
* 配置 HttpSecurity
* @param http
* @throws Exception
* 特性 antMatchers() mvcMatchers()
* 匹配规则 简单的 Ant 风格模式匹配 遵循 Spring MVC 的路径匹配规则
* 路径变量处理 不支持 @PathVariable 解析 支持 @PathVariable 解析
* 后缀匹配 默认不处理后缀 默认会处理后缀(如 .html, .json)
* 斜杠处理 严格匹配,/path ≠ /path/ 宽松匹配,/path ≈ /path/
* 性能 稍快 稍慢(因为要处理更多规则)
http.authorizeRequests()
// antMatchers 示例
.antMatchers("/api/**").hasRole("ADMIN") // 精确匹配 /api/ 开头的路径
.antMatchers("/user/{id}").permitAll() // {id} 只是通配符,不是路径变量
// mvcMatchers 示例
.mvcMatchers("/product/{id}").authenticated() // 能匹配 /product/123 和 /product/123/
.mvcMatchers("/admin").hasAuthority("ROLE_ADMIN"); // 能匹配 /admin 和 /admin.html
*/
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 这里代表开启请求的权限管理
.mvcMatchers("/login.html").permitAll() // 放行 login 页面
.mvcMatchers("/index").permitAll() // 放行 index 页面
.anyRequest().authenticated()
.and()
.formLogin() // 开启表单登录 并使用默认的登录页面 使用自定义的登录页面使用 .loginPage("/login")
.loginPage("/login.html").usernameParameter("username").passwordParameter("password")
.loginProcessingUrl("/doLogin")
// .defaultSuccessUrl("/index") // 登录成功后跳转的页面
// .defaultSuccessUrl("/index", true) // 登录成功后跳转的页面 , 作为一个erp系统的登录, 登录成功强制跳到首页
// .successForwardUrl("/index") // 登录成功后跳转的页面 Forward 跳转, 跳转到 /index 再访问其他页面会一直是这个地址
.successHandler(new MyAuthenticationSuccessHandler()) // 登录成功后自定义处理, 前后端分离, 返回 json 数据
.failureUrl("/login.html?error") // 登录失败后跳转的页面
.and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler()) // MyLogoutSuccessHandler
.logoutUrl("/logout") // 注销登录的 url
// .logoutSuccessUrl("/login.html?logout") // 注销登录成功后跳转的页面
.and().csrf().disable(); // 关闭跨域攻击的防护 测试时暂时关闭
}
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication().withUser("user").password("{noop}123456").roles("USER");
// }
}
(。・v・。)