连锁书店管理系统开发遇到的一些问题

Spring Sceurity未在SecurityConfig中加入jwt过滤器导致登录时需要jwt令牌

解决:

在SecurityConfig提前加入jwt过滤器,并放行login接口

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable() // 禁用 CSRF
            .authorizeHttpRequests()
            .requestMatchers("/manage/login").permitAll() // 允许匿名访问登录接口
            .anyRequest().authenticated() // 其他请求需要认证
            .and()
            // 将 JwtFilter 添加到过滤器链中,放在 UsernamePasswordAuthenticationFilter 之前
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

为什么要把 JwtFilter 放在 UsernamePasswordAuthenticationFilter 之前?

优先级问题

  • UsernamePasswordAuthenticationFilter 是专门处理表单登录的过滤器。如果请求需要基于 Token 的认证(如 JWT),那么 JwtFilter 应该优先处理请求。
  • 如果 UsernamePasswordAuthenticationFilter 先执行,它会尝试处理所有请求,即使这些请求是通过 Token 认证的。这会导致不必要的认证逻辑执行,甚至可能覆盖 JwtFilter 设置的认证信息。

避免冲突

  • 如果请求携带了有效的 Token,JwtFilter 会直接设置 Authentication 对象并放行请求。这样,UsernamePasswordAuthenticationFilter 就不会被调用,避免了可能存在的冲突。
  • 如果请求是登录请求(如 /api/login),JwtFilter 会放行请求,UsernamePasswordAuthenticationFilter 可以正常处理登录逻辑。

效率问题

  • JwtFilter 通常只需要解析 Token 并验证其有效性,效率较高。如果先调用 UsernamePasswordAuthenticationFilter,它可能会执行不必要的用户名密码认证逻辑,影响性能。

authenticationManager.authenticate()循环调用loadUserByUsername()方法

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisService redisService;
    @Override
    public Result login(StaffLogin staffLogin) {

        //AuthenticationManager authenticate进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(staffLogin.getUsername(), staffLogin.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //如果认证没通过,给出对应的提示
        if (authenticate == null || !authenticate.isAuthenticated()) {
            return Result.unauthorized("用户名或密码错误",null);
        }
        //如果认证通过,使用userid生成一个jwt jwt存入StaffInfo返回
        LoginStaff loginStaff = (LoginStaff) authenticate.getPrincipal();
        //生成jwt
        Long id = loginStaff.getStaff().getId();
        String jwtToken = genAccessToken(id);
        //把完整的员工信息存入redis userid作为key
        redisService.setValueWithExpire("Staff:"+id,loginStaff,3600);
        //封装返回前端
        List<String> authorityList = loginStaff.getPermissions();
        StaffInfo staffInfo = loginStaff.getStaff().toStaffInfo(authorityList,jwtToken);
        return Result.success("登录成功",staffInfo);
    }
}

原因:使用了 @Bean 注解来创建 UserDetailsServiceImpl,但同时该类已经被标记为 @Service 注解。这会导致 Spring 容器中存在两个 UserDetailsService 实例,造成循环调用。

问题分析

  1. 重复实例化的问题
    • 你的 UserDetailsServiceImpl 类有 @Service 注解,这会使 Spring 自动创建一个实例并管理它
    • 同时,在 SecurityConfig 中你又使用 @Bean 方法 userDetailsService() 手动创建了一个实例
    • 结果是 Spring 容器中有两个 UserDetailsServiceImpl 实例
  2. 在 Spring Security 中的影响
    • 当有多个 UserDetailsService Bean 时,Spring Security 可能会在不同的地方使用不同的实例
    • 这会导致认证流程混乱,可能造成循环引用或循环调用
    • authenticationManager.authenticate() 方法会使用其中一个实例,而其他地方可能使用另一个实例

解决方案

修改你的代码,确保只有一个 UserDetailsService 实例:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private JwtFilter jwtFilter;

    @Autowired
    private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    
    @Autowired
    private UserDetailsServiceImpl userDetailsService; // 直接注入已有的 @Service 实例

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

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        // 使用注入的 userDetailsService 实例,而不是创建新的
        authenticationManagerBuilder
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
        return authenticationManagerBuilder.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 其余配置保持不变
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests()
                .requestMatchers(PermitAllPaths.getPaths().toArray(new String[0])).permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(customAuthenticationEntryPoint)
                .accessDeniedHandler(customAccessDeniedHandler);

        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

这样修改后,你将:

  1. 删除了 @Bean public UserDetailsServiceImpl userDetailsService() 方法
  2. 改为直接 @Autowired 注入由 @Service 注解自动创建的单例
  3. authenticationManager 方法中使用注入的实例
posted @ 2025-03-06 14:41  NONAME-X  阅读(24)  评论(0)    收藏  举报