为何@PreAuthorize中principal是String而非UserDetails类?
场景复现
我在方法上标注了@PreAuthorize注解,调用自定义bean做权限校验:
@PreAuthorize("@preAuthUtils.test(authentication)") public void method(){ ... }
自定义bean仅用于打印认证信息和用户主体:
@Component(value = "preAuthUtils") public class PreAuthUtils { public boolean test(UsernamePasswordAuthenticationToken x) { System.out.println("type: " + x.getClass().getSimpleName()); System.out.println("string: " + x.toString()); final var user = x.getPrincipal(); System.out.println("type: " + user.getClass().getSimpleName()); System.out.println("string: " + user.toString()); return true; } }
请求后输出显示,Principal的类型是String而非我实现了UserDetails接口的DefaultUserDetails:
type: UsernamePasswordAuthenticationToken string: UsernamePasswordAuthenticationToken [Principal=johndoe, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_USER, ROLE_CUSTOMER]] type: String string: johndoe
我需要获取用户ID及相关信息来判断访问权限,却一直无法拿到自定义UserDetails实例,十分困惑。
原因分析
出现这种情况的核心原因是:Spring Security的认证流程中,没有将自定义UserDetails实例设置到Authentication对象的Principal中,常见触发场景包括:
- 自定义
UserDetailsService的loadUserByUsername方法没有返回DefaultUserDetails实例,而是直接返回了用户名字符串 - 未正确配置Spring Security使用自定义的
UserDetailsService,导致框架使用默认逻辑只存储用户名作为Principal - 自定义认证过滤器时,认证成功后手动将Principal替换成了字符串
解决步骤
1. 检查并修正UserDetailsService实现
确保loadUserByUsername方法返回的是你的DefaultUserDetails实例,而非字符串:
@Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 从数据库查询用户完整信息 UserEntity user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("用户不存在")); // 返回自定义DefaultUserDetails,封装用户ID等信息 return new DefaultUserDetails( user.getId(), user.getUsername(), user.getPassword(), AuthorityUtils.createAuthorityList(user.getRoles().toArray(new String[0])) ); } }
2. 配置Spring Security使用自定义UserDetailsService
在Security配置类中明确指定自定义的UserDetailsService和密码编码器,确保框架使用你的逻辑加载用户:
@Configuration @EnableWebSecurity public class SecurityConfig { private final CustomUserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder; public SecurityConfig(CustomUserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { this.userDetailsService = userDetailsService; this.passwordEncoder = passwordEncoder; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .formLogin(form -> form.permitAll()) // 绑定自定义UserDetailsService和密码编码器 .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder); return http.build(); } // 配置密码编码器示例 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
3. 优化PreAuthUtils的参数兼容性
为了适配更多Authentication类型,建议将方法参数改为Authentication接口,再强制转换为自定义UserDetails:
@Component(value = "preAuthUtils") public class PreAuthUtils { public boolean test(Authentication authentication) { System.out.println("type: " + authentication.getClass().getSimpleName()); System.out.println("string: " + authentication.toString()); // 转换为自定义DefaultUserDetails,获取用户ID等信息 DefaultUserDetails user = (DefaultUserDetails) authentication.getPrincipal(); System.out.println("type: " + user.getClass().getSimpleName()); System.out.println("string: " + user.toString()); System.out.println("用户ID: " + user.getId()); // 这里可以获取自定义属性 return true; } }
内容的提问来源于stack exchange,提问作者user19926584
相关产品推荐
相关产品推荐

