Spring中@AuthenticationPrincipal注入OidcUser为null的问题
我在Spring应用中整合Keycloak实现认证功能时遇到了问题:调用接口时,@AuthenticationPrincipal注解注入的OidcUser始终为null,但通过SecurityContextHolder能正常获取到Authentication信息。
接口代码
@GetMapping("/api/user/name") public ResponseEntity<?> getUserName(@AuthenticationPrincipal OidcUser user) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); System.out.println("Authentication: " + authentication); if (user != null) { return ResponseEntity.ok(user); } else { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("User not authenticated"); } }
Spring安全配置代码
@SpringBootApplication public class TestOauth2Application { public static void main(String[] args) { SpringApplication.run(TestOauth2Application.class, args); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); http.oauth2Login(Customizer.withDefaults()); http.logout(logout -> logout .logoutSuccessUrl("/") .permitAll()); return http .authorizeHttpRequests(c -> c .requestMatchers("/error").permitAll() .requestMatchers("/manager.html").hasRole("USER") .requestMatchers("/test/test").hasRole("USER") .anyRequest().authenticated()) .build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { var converter = new JwtAuthenticationConverter(); var jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); converter.setPrincipalClaimName("sub"); converter.setJwtGrantedAuthoritiesConverter(jwt -> { var authorities = jwtGrantedAuthoritiesConverter.convert(jwt); var roles = jwt.getClaimAsStringList("spring_sec_roles"); return Stream.concat(authorities.stream(), roles.stream() .filter(role -> role.startsWith("ROLE_")) .map(SimpleGrantedAuthority::new) .map(GrantedAuthority.class::cast)) .toList(); }); return converter; } @Bean public OAuth2UserService<OidcUserRequest, OidcUser> oAuth2UserService() { var oidcUserService = new OidcUserService(); return userRequest -> { var oidcUser = oidcUserService.loadUser(userRequest); var roles = oidcUser.getClaimAsStringList("spring_sec_roles"); var authorities = Stream.concat(oidcUser.getAuthorities().stream(), roles.stream() .filter(role -> role.startsWith("ROLE_")) .map(SimpleGrantedAuthority::new) .map(GrantedAuthority.class::cast)) .toList(); var nameAttribute = userRequest.getClientRegistration() .getProviderDetails() .getUserInfoEndpoint() .getUserNameAttributeName(); var name = oidcUser.getAttribute(nameAttribute); oidcUser.getAttributes().put("name", name); return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo()); }; } }
请求使用的Token
Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ6eTdPbVN1b3NTVGJCUlhEYnRzZFBtOUtDV3FBQWZhQy1iMzh6RHBVOTlVIn0.eyJleHAiOjE3MDM3MjM2NDEsImlhdCI6MTcwMzcyMjc0MSwiYXV0aF90aW1lIjoxNzAzNzIyNzQxLCJqdGkiOiI3YmE0YmQ2NC1mMDQ5LTRjMjgtYjBkMS1jODQ3NWEyNWIwYTUiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvcmVhbG1zL2thc2lhIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjJhYTYyM2Y4LTI3NTEtNDA1Mi05ZDBkLTNmMWQwNzhiNmU5MCIsInR5cCI6IkJlYXJlciIsImF6cCI6Imthc2lhLXNlY3VyaXR5Iiwibm9uY2UiOiJiYzUxYWE5MC0zMTA0LTQ5ZjEtOGNmYy01ZDM4N2NlOTE4NGQiLCJzZXNzaW9uX3N0YXRlIjoiNjIxMThmMTUtNDE1OC00YzlmLWEyZDItZTNmYzRiMGIwMThhIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJodHRwOi8vbG9jYWxob3N0OjMwMDAvIiwiaHR0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1rYXNpYSIsIlJPTEVfVVNFUiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJVU0VSIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtbWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUgIiwic2lkIjoiNjIxMThmMTUtNDE1OC00YzlmLWEyZDItZTNmYzRiMGIwMThhIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiQXJ0ZW0gRHViZW5rbyIsInNwcmluZ19zZWNyZXRfaWQiOiI2MjExOGYxNS00MTU4LTRjOWYtYTJkMi1lM2ZjNGIwYjAxOGEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNvbnRhY3Qtb3JpZ2lucyI6W3sicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfV0sInNjb3BlIjoiTm9ybWFsIEFwcCIsInNpZCI6IjYyMTE4ZjE1LTQxNTgtNGM5Zi1hMmQyLWUzZmM0YjBiMDE4YSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkFydGVtIER1YmVua28iLCJzcHJpbmdfc2VjX3JvbGVzIjpbImRlZmF1bHQtcm9sZXMta2FzaWEiLCJST0xFX1VTRVIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiVVNFUiJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhcnRlbS5kdWJlbmtvMDMwOEBnbWFpbC5jb20iLCJnaXZlbl9uYW1lI
补充说明
调用接口时,SecurityContextHolder.getContext().getAuthentication()能正常获取到Authentication信息,但@AuthenticationPrincipal OidcUser user始终为null,请问哪里出了问题?
问题原因
你同时配置了oauth2ResourceServer(jwt)和oauth2Login,这两个场景对应的认证主体类型完全不同:
- 用Bearer Token调用接口属于资源服务器模式,Spring Security会用JWT认证逻辑,此时Authentication的主体是
JwtAuthenticationToken,对应的实例是Jwt对象,而非OidcUser OidcUser是OAuth2授权码登录模式下的用户类型,只有通过浏览器跳转登录、会话中保存认证信息时,接口才能拿到OidcUser类型的主体
你的请求是用Bearer Token调用,属于资源服务器场景,所以@AuthenticationPrincipal注入OidcUser会失败,因为当前主体类型不匹配。
解决方法
根据你的业务场景选择对应方案:
1. 适配Bearer Token调用场景(资源服务器模式)
把参数类型改成Jwt,直接从JWT中提取用户信息:
@GetMapping("/api/user/name") public ResponseEntity<?> getUserName(@AuthenticationPrincipal Jwt jwt) { String userName = jwt.getClaim("preferred_username"); String userId = jwt.getSubject(); return ResponseEntity.ok(Map.of("userId", userId, "userName", userName)); }
或者继续用SecurityContextHolder获取Authentication,再强转成JwtAuthenticationToken提取信息:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication; Jwt jwt = jwtToken.getToken(); // 处理JWT中的用户信息
2. 适配OAuth2登录会话场景(授权码模式)
如果你的接口是给登录后的前端页面调用,确保请求携带会话Cookie(不要用Bearer Token),此时Spring Security会从会话中读取OidcUser类型的认证主体,@AuthenticationPrincipal OidcUser user就能正常注入。
另外,你配置的jwtAuthenticationConverter会把JWT转换成JwtAuthenticationToken,进一步确认了资源服务器模式下的主体是Jwt而非OidcUser。
内容的提问来源于stack exchange,提问作者Artem Dubenko

