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

Spring中@AuthenticationPrincipal注入OidcUser为null的问题

Spring整合Keycloak时@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 09:20:56