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

如何在未受保护接口中获取@AuthenticationPrincipal用户信息?

问题描述

我有一个继承自org.springframework.security.core.userdetails.User的AuthenticatedUser类,由JWT构造生成。浏览器发起的每个请求都会附带JWT,接口分为受保护和公开两种:

  • 受保护接口中,@AuthenticationPrincipal AuthenticatedUser principal参数能正常填充用户信息;
  • 公开接口中,即便请求携带了JWT,该参数始终为null。

现在需要实现:在公开接口中也能获取到该用户信息。

现有配置代码

ResourceServerConfig

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    private final List<String> protectedPaths = List.of(
            "/secret/*/**",
            "/private"
    );
    // 若将公开路径加入此列表,principal会被填充,但匿名用户无法访问该公开链接;不加入则principal为null。

    @Bean
    @Order(2)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatchers(customizer -> customizer.requestMatchers(protectedPaths.toArray(new String[0])))
                .csrf().disable()
                .authorizeHttpRequests(requests -> requests.anyRequest().authenticated())
                .oauth2ResourceServer()
                .jwt(customizer -> customizer.jwtAuthenticationConverter(new UserAuthenticationTokenConverter()));
        return http.build();
    }
}

控制器代码

@GetMapping(path = "/public/{id}")
public ListingDetailedView
findById(@PathVariable String id, @AuthenticationPrincipal AuthenticatedUser principal) {
    // 此处principal为null
    return listingService.findBySearchId(id, principal);
}

补充的PartnerResourceServerConfig

@Configuration
@RequiredArgsConstructor
public class PartnerResourceServerConfig {

    private final PartnerUserDetailsService userDetailsService;
    private final AuthenticationFailureHandler failureHandler;

    @Bean
    @Order(1)
    public SecurityFilterChain partnerSecurityFilterChain(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
        authManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        var authenticationManager = authManagerBuilder.build();

        http.securityMatcher("/hidden/**")
                .authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated())
                .authenticationManager(authenticationManager)
                .addFilterBefore(partnerApiKeyAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class)
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManagementCustomizer -> sessionManagementCustomizer
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }

    PartnerApiKeyAuthenticationFilter partnerApiKeyAuthenticationFilter(AuthenticationManager authenticationManager) {
        var filter = new PartnerApiKeyAuthenticationFilter("/hidden/**");
        filter.setAuthenticationFailureHandler(failureHandler);
        filter.setAuthenticationManager(authenticationManager);
        return filter;
    }

}

附加类代码

UserAuthenticationTokenConverter
@NoArgsConstructor
public class UserAuthenticationTokenConverter implements Converter<Jwt, AuthenticationToken> {

    @Override
    public AuthenticationToken convert(Jwt source) {
        var authenticatedUser = AuthenticatedUser.from(source);
        return new AuthenticationToken(source, authenticatedUser);
    }
}
AuthenticationToken
public class AuthenticationToken extends AbstractAuthenticationToken {

    private final Jwt jwt;
    private final AuthenticatedUser authenticatedUser;

    public AuthenticationToken(Jwt jwt, AuthenticatedUser authenticatedUser) {
        super(List.of());
        this.jwt = jwt;
        this.authenticatedUser = authenticatedUser;
    }

    @Override
    public Object getCredentials() {
        return this.jwt;
    }

    @Override
    public Object getPrincipal() {
        return authenticatedUser;
    }

    @Override
    public boolean isAuthenticated() {
        return true;
    }
}
AuthenticatedUser
@Getter
public class AuthenticatedUser extends User {

    private final String id;
    private final UserRole role;
    private final String email;

    private final boolean isPhoneVerified;

    private AuthenticatedUser(String id, UserRole role, String email, String token,
                              boolean isPhoneVerified) {
        super(id, token, true, true, true, true, List.of());
        this.id = id;
        this.role = role;
        this.email = email;
        this.isPhoneVerified = isPhoneVerified;
    }

    @SuppressWarnings("unchecked")
    public static AuthenticatedUser from(Jwt jwt) {
        var id = jwt.getSubject();
        var roleClaim = jwt.getClaimAsString("type");
        var userRole = UserRole.forValue(roleClaim);
        var userEmail = jwt.getClaimAsString("email");
        var isPhoneVerifiedString = jwt.getClaimAsBoolean("phone_number_verified");
        var isPhoneVerified = isPhoneVerifiedString != null ? isPhoneVerifiedString : false;
        return new AuthenticatedUser(id, userRole, userEmail, jwt.getTokenValue(), isPhoneVerified);
    }
}

解决方案

核心思路是让Spring Security对公开路径执行JWT解析逻辑,但不强制要求用户必须认证(允许匿名访问,同时若有合法JWT则自动解析并填充AuthenticationPrincipal)。以下两种方式均可实现:

方式一:修改现有SecurityFilterChain,覆盖所有路径并配置权限规则

调整ResourceServerConfig中的过滤器链,让它匹配所有路径,再分别配置受保护路径和公开路径的权限规则:

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    private final List<String> protectedPaths = List.of(
            "/secret/*/**",
            "/private"
    );

    @Bean
    @Order(2)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 匹配所有请求路径
                .securityMatchers(customizer -> customizer.requestMatchers("/**"))
                .csrf().disable()
                .authorizeHttpRequests(requests -> requests
                        // 受保护路径必须认证
                        .requestMatchers(protectedPaths.toArray(new String[0])).authenticated()
                        // 其余公开路径允许匿名访问(携带合法JWT时会自动解析)
                        .anyRequest().permitAll()
                )
                .oauth2ResourceServer()
                .jwt(customizer -> customizer.jwtAuthenticationConverter(new UserAuthenticationTokenConverter()));
        return http.build();
    }
}

方式二:新增专门处理公开路径的SecurityFilterChain

如果不想修改原有配置,可新增一个优先级更低的过滤器链,专门处理公开路径:

@Configuration
public class PublicResourceServerConfig {

    @Bean
    @Order(3) // 优先级需低于现有两个过滤器链(数字越大优先级越低)
    public SecurityFilterChain publicSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                // 匹配所有公开路径
                .securityMatchers(customizer -> customizer.requestMatchers("/public/**"))
                .csrf().disable()
                // 允许匿名访问
                .authorizeHttpRequests(requests -> requests.anyRequest().permitAll())
                // 启用JWT解析
                .oauth2ResourceServer()
                .jwt(customizer -> customizer.jwtAuthenticationConverter(new UserAuthenticationTokenConverter()));
        return http.build();
    }
}

关键说明

  • 两种方式的核心都是让JwtAuthenticationFilter对公开路径生效,Spring Security会自动解析请求中的JWT并将认证信息存入SecurityContext,从而让@AuthenticationPrincipal能获取到用户信息;
  • 配置.permitAll()后,无JWT的匿名请求也能正常访问公开接口,不会被拦截;
  • 注意过滤器链的优先级:现有PartnerResourceServerConfig优先级为1,ResourceServerConfig为2,新增的公开路径过滤器链优先级需设为更大的数字,避免被其他过滤器提前拦截。

内容的提问来源于stack exchange,提问作者ilhan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 16:43:18