基于Spring Security JWT的多UserDetailsService认证实现问询
Spring Security 同时支持 User 和 Employee 双模型 JWT 认证
问题背景
当前Web应用拥有User和Employee两个独立领域模型,已实现User的JWT认证,现需扩展支持Employee的身份认证,核心疑问如下:
- 是否需要为Employee创建独立的UserDetailsService实现类?
- 是否需要在MyUserDetails中新增构造方法以适配Employee?
- 如何在配置类中配置以兼容两种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中添加userTypeClaim,认证时直接根据该字段选择对应的Service。 - 登录接口可新增
userType参数,根据参数选择对应Service生成JWT,提升认证效率。
内容的提问来源于stack exchange,提问作者ServletException
相关产品推荐
相关产品推荐

