如何在未受保护接口中获取@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
相关产品推荐
相关产品推荐

