Spring Boot 2/Security 5中@AuthenticationPrincipal空值、@PreAuthorize失效问题
你的核心问题是登录成功后@AuthenticationPrincipal无法注入用户对象,进而导致@PreAuthorize注解无法正常生效。结合你提供的代码,我整理了几个关键排查点和解决方案:
1. 类型不匹配导致注入失败
你控制器中@AuthenticationPrincipal User user里的User是业务实体类,但Spring Security的SecurityContext中存储的是UserDetails接口的实现类(你在UserDetailsServiceImpl中返回的是org.springframework.security.core.userdetails.User)。这两个类型不匹配,Spring无法自动注入,因此返回Null。
解决方法:
有两种常见处理方式:
方式一:直接使用UserDetails作为参数
修改控制器方法,先通过UserDetails获取用户名,再查询业务用户:
@RequestMapping(value = "/self",method = RequestMethod.GET) public ResponseEntity<User> getLogedInUser(@AuthenticationPrincipal UserDetails userDetails){ User businessUser = service.getUserByName(userDetails.getUsername()); return new ResponseEntity<>(businessUser, HttpStatus.OK); }
方式二:自定义UserDetails实现类,封装业务用户
创建自定义UserDetails实现,把业务User对象包含进去:
public class CustomUserDetails extends org.springframework.security.core.userdetails.User { private final User businessUser; public CustomUserDetails(User businessUser, Collection<? extends GrantedAuthority> authorities) { super(businessUser.getName(), businessUser.getPassword(), authorities); this.businessUser = businessUser; } public User getBusinessUser() { return businessUser; } }
然后在UserDetailsServiceImpl中返回这个自定义类:
@Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findUserByNameEquals(username) .orElseThrow(() -> new UsernameNotFoundException(errorMessage)); HashSet<GrantedAuthority> authorities = new HashSet<>(); if(user.getRoles() != null){ user.getRoles().stream() .map(Role::getName) .map(SimpleGrantedAuthority::new) .forEach(authorities::add); } return new CustomUserDetails(user, authorities); }
最后在控制器中通过表达式注入业务用户:
@RequestMapping(value = "/self",method = RequestMethod.GET) public ResponseEntity<User> getLogedInUser(@AuthenticationPrincipal(expression = "businessUser") User user){ return new ResponseEntity<>(user, HttpStatus.OK); }
2. 自定义登录过滤器未正确设置SecurityContext
你的RESTAuthenticationFilter是自定义登录过滤器,需确保认证成功后将Authentication对象存入SecurityContextHolder,否则后续请求无法获取认证信息。
检查RESTAuthenticationFilter是否重写了successfulAuthentication方法,若没有,添加以下逻辑:
@Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { // 将认证结果存入SecurityContext SecurityContextHolder.getContext().setAuthentication(authResult); // 调用成功处理器 getSuccessHandler().onAuthenticationSuccess(request, response, authResult); // 继续执行过滤器链 chain.doFilter(request, response); }
如果过滤器继承了UsernamePasswordAuthenticationFilter,直接调用父类的successfulAuthentication方法即可,它会自动处理SecurityContext的存储。
3. 验证@PreAuthorize的角色匹配问题
注意hasRole('USER')会自动给角色名称添加ROLE_前缀,若你数据库中存储的角色名称是USER(无前缀),hasRole('USER')实际会匹配ROLE_USER,导致权限校验失败。
若角色名称无前缀,可改用hasAuthority('USER'):
@PreAuthorize("hasAuthority('USER')") public ResponseEntity<List<User>> getAllUsers() { return new ResponseEntity<>(service.getAllUsers(), HttpStatus.OK); }
或者在配置中移除默认前缀:
@Bean public GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // 移除默认的ROLE_前缀 }
4. 确认SecurityContext的存储策略
由于你使用基于Session的认证,需确保SecurityContext被正确存储到HttpSession中。Spring Security默认通过HttpSessionSecurityContextRepository处理该逻辑,但如果自定义过滤器未正确触发存储,会导致后续请求无法获取认证信息。
可在登录成功处理器中添加调试代码,检查Session中是否存在SPRING_SECURITY_CONTEXT属性,确认存储是否正常。
内容的提问来源于stack exchange,提问作者Yanick Schraner

