You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Spring Security JWT的多UserDetailsService认证实现问询

Spring Security 同时支持 User 和 Employee 双模型 JWT 认证

问题背景

当前Web应用拥有User和Employee两个独立领域模型,已实现User的JWT认证,现需扩展支持Employee的身份认证,核心疑问如下:

  1. 是否需要为Employee创建独立的UserDetailsService实现类?
  2. 是否需要在MyUserDetails中新增构造方法以适配Employee?
  3. 如何在配置类中配置以兼容两种UserDetailsService?

原有代码实现

MyUserDetailsService

@Service
@AllArgsConstructor
public class MyUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String emailAddress) throws UsernameNotFoundException {

        User user = userRepository.findByEmailAddress(emailAddress);

        if(user == null) {
            throw new UsernameNotFoundException("Invalid credentials / Account not found");
        }
        return MyUserDetails.createAppUser(user);
    }

}

MyUserDetails

public class MyUserDetails implements UserDetails {

        private User user;
        private Collection<? extends GrantedAuthority> authorities;

        public MyUserDetails(User user, Collection<? extends GrantedAuthority> authorities) {
            this.user = user;
            this.authorities = authorities;
        }
        
        public static MyUserDetails createAppUser(User user) {
            List<GrantedAuthority> authorities = user.getRoles().stream()
                    .map(role -> new SimpleGrantedAuthority(role.getName().name()))
                    .collect(Collectors.toList());

            return new MyUserDetails(user, authorities);
        }


        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }

        @Override
        public String getPassword() {
            return user.getPassword();
        }

        @Override
        public String getUsername() {
            return user.getEmailAddress();
        }

        @Override
        public boolean isAccountNonExpired() {
            return true;
        }

        @Override
        public boolean isAccountNonLocked() {
            return true;
        }

        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }

        @Override
        public boolean isEnabled() {
            return true;
        }
    }

JWT Filter

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtServiceImpl jwtService;
    private final MyUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
            throws ServletException, IOException {

        String token = jwtService.getToken(request) ;

        if (token!=null && jwtService.validateToken(token)) {

            String email = jwtService.extractUserName(token);
            Claims claims = jwtService.extractAllClaims(token);


            UserDetails userDetails = userDetailsService.loadUserByUsername(email);

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails.getUsername() ,null , userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);

        }
        filterChain.doFilter(request,response);
    }
}

WebConfig

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Value("${spring.security.ant.matchers}")
    private String[] securityAntMatchers;

    private final JwtAuthenticationFilter authFilter;

    @Bean
    public UserDetailsService userDetailsService() {
        return new MyUserDetailsService();
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeHttpRequests()
                .requestMatchers(securityAntMatchers).permitAll()
                .anyRequest().authenticated()
                .and()
                .cors().disable()
                .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

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

    @Bean
    public AuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService());
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }


    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

User 实体

@Entity
@Getter
@Setter
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private int userId;
    @Embedded
    private Name name;

    private LocalDate dateJoined;
    private String password;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "user")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Set<ContactDetail> contactDetails = new HashSet<>();

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "user_roles",
            joinColumns = @JoinColumn(name="user_id"),
            inverseJoinColumns = @JoinColumn(name="role_id"))
    private Set<Role> roles = new HashSet<>();

    private String emailAddress;
}

Employee 实体

@Entity
@Getter
@Setter
public class Employee {

    @Id
    @Column(name = "employee_number")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int employeeNumber;
    @Embedded
    private Name name;
    private LocalDateTime dateJoined;
    @NotBlank(message = "Required")
    private String password;


    @OneToMany(fetch = FetchType.EAGER, mappedBy = "employee", cascade = CascadeType.ALL)
    private List<Post> posts = new ArrayList<>();

   @OneToMany(fetch = FetchType.EAGER, mappedBy = "user")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Set<ContactDetail> contactDetails = new HashSet<>();

    private String emailAddress;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "employee_roles",
            joinColumns = @JoinColumn(name="employee_id"),
            inverseJoinColumns = @JoinColumn(name="role_id"))
    private Set<Role> roles = new HashSet<>();

}

问题解答

1. 是否需要为Employee创建独立的UserDetailsService实现类?

需要。User和Employee是独立实体,对应不同的Repository,单独实现UserDetailsService能保证职责单一,也便于后续各自的功能扩展。可以创建EmployeeUserDetailsService专门处理Employee的加载逻辑。

2. 是否需要在MyUserDetails中新增构造方法以适配Employee?

需要。当前MyUserDetails仅持有User对象,需新增针对Employee的构造方法和静态工厂方法,让它能同时封装两种实体的认证信息。

3. 如何在配置类中配置以兼容两种UserDetailsService?

不能直接配置多个UserDetailsService作为单一Bean,可通过两种方式整合:

  • 配置多个AuthenticationProvider,分别对应两个UserDetailsService,支持表单登录等多种认证场景;
  • 在JWT认证过滤器中,先尝试从User加载,失败再尝试Employee,或通过JWT中的自定义Claim(如userType)直接指定加载的Service。

修改后的代码实现

1. 扩展MyUserDetails支持Employee

public class MyUserDetails implements UserDetails {

    private User user;
    private Employee employee;
    private Collection<? extends GrantedAuthority> authorities;

    // 保留原User构造方法
    public MyUserDetails(User user, Collection<? extends GrantedAuthority> authorities) {
        this.user = user;
        this.authorities = authorities;
    }

    // 新增Employee构造方法
    public MyUserDetails(Employee employee, Collection<? extends GrantedAuthority> authorities) {
        this.employee = employee;
        this.authorities = authorities;
    }

    // 保留原User工厂方法
    public static MyUserDetails createAppUser(User user) {
        List<GrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName().name()))
                .collect(Collectors.toList());
        return new MyUserDetails(user, authorities);
    }

    // 新增Employee工厂方法
    public static MyUserDetails createEmployee(Employee employee) {
        List<GrantedAuthority> authorities = employee.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName().name()))
                .collect(Collectors.toList());
        return new MyUserDetails(employee, authorities);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return user != null ? user.getPassword() : employee.getPassword();
    }

    @Override
    public String getUsername() {
        return user != null ? user.getEmailAddress() : employee.getEmailAddress();
    }

    // 其他UserDetails方法保持不变
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // 可选:获取原始实体,便于业务逻辑使用
    public Object getOriginalUser() {
        return user != null ? user : employee;
    }
}

2. 创建EmployeeUserDetailsService

@Service
@AllArgsConstructor
public class EmployeeUserDetailsService implements UserDetailsService {

    private final EmployeeRepository employeeRepository;

    @Override
    public UserDetails loadUserByUsername(String emailAddress) throws UsernameNotFoundException {
        Employee employee = employeeRepository.findByEmailAddress(emailAddress);
        if (employee == null) {
            throw new UsernameNotFoundException("Employee not found with email: " + emailAddress);
        }
        return MyUserDetails.createEmployee(employee);
    }
}

3. 修改JwtAuthenticationFilter适配双Service

这里采用"先尝试User,失败再尝试Employee"的策略:

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtServiceImpl jwtService;
    private final MyUserDetailsService userDetailsService;
    private final EmployeeUserDetailsService employeeUserDetailsService;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
            throws ServletException, IOException {

        String token = jwtService.getToken(request);

        if (token != null && jwtService.validateToken(token)) {
            String email = jwtService.extractUserName(token);
            UserDetails userDetails = null;

            // 先尝试加载User
            try {
                userDetails = userDetailsService.loadUserByUsername(email);
            } catch (UsernameNotFoundException e) {
                // User不存在,尝试加载Employee
                try {
                    userDetails = employeeUserDetailsService.loadUserByUsername(email);
                } catch (UsernameNotFoundException ex) {
                    // 两者都不存在,直接跳过
                    filterChain.doFilter(request, response);
                    return;
                }
            }

            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}

4. 修改SecurityConfig配置双AuthenticationProvider

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Value("${spring.security.ant.matchers}")
    private String[] securityAntMatchers;

    private final JwtAuthenticationFilter authFilter;
    private final MyUserDetailsService userDetailsService;
    private final EmployeeUserDetailsService employeeUserDetailsService;

    // User对应的认证提供者
    @Bean
    public AuthenticationProvider userAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    // Employee对应的认证提供者
    @Bean
    public AuthenticationProvider employeeAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(employeeUserDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeHttpRequests()
                .requestMatchers(securityAntMatchers).permitAll()
                .anyRequest().authenticated()
                .and()
                .cors().disable()
                .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // 注册两个认证提供者
        http.authenticationProvider(userAuthenticationProvider())
            .authenticationProvider(employeeAuthenticationProvider());

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

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

补充说明

  • 确保User和Employee的邮箱地址不重复,避免认证混淆;若允许重复,需在JWT中添加userType Claim,认证时直接根据该字段选择对应的Service。
  • 登录接口可新增userType参数,根据参数选择对应Service生成JWT,提升认证效率。

内容的提问来源于stack exchange,提问作者ServletException

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 22:49:52