首页 » java

Spring Security 用户认证配置深度解析

   发表于:java评论 (0)   热度:3

一、核心配置方案对比

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・。)
喜欢这篇文章吗?欢迎分享到你的微博、QQ群,并关注我们的微博,谢谢支持。