Spring Security自定义Filter获取认证Principal为空的问题排查
我在Spring Security中配置了如下SecurityFilterChain:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { final CorsConfigurationSource configurationSource = serverWebExchange -> { final var cc = new CorsConfiguration(); cc.addAllowedOriginPattern(CorsConfiguration.ALL); cc.addAllowedMethod(CorsConfiguration.ALL); cc.addAllowedHeader(CorsConfiguration.ALL); return cc; }; // Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2Customizer = (config) -> config.opaqueToken(); Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2Customizer = (config) -> config.jwt(); return http .httpBasic().disable() .csrf().disable() .formLogin().disable() .anonymous().disable() .logout().disable() .cors().configurationSource(configurationSource).and() .authorizeHttpRequests((authorize) -> authorize .antMatchers("/actuator/**").permitAll() .antMatchers("/gicar/**").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2Customizer) .addFilterAfter(new CommonsHeaderArePresentFilter(this.httpServletRequest), BearerTokenAuthenticationFilter.class) .build(); }
核心配置是将自定义过滤器CommonsHeaderArePresentFilter添加在BearerTokenAuthenticationFilter之后,这个过滤器用来验证请求参数user和token中的clientId自定义声明是否匹配。自定义过滤器代码如下:
@RequiredArgsConstructor @Slf4j public class CommonsHeaderArePresentFilter extends GenericFilterBean { private final HttpServletRequest httpServletRequest; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String principal = this.httpServletRequest.getUserPrincipal().getName(); log.info(principal); HttpServletRequest req = (HttpServletRequest) request; Optional<String> userRequestParameter = Optional.ofNullable(req.getParameter("user")); if(userRequestParameter.isPresent()){ chain.doFilter(request, response); }else{ HttpServletResponse resp = (HttpServletResponse) response; String error = "Invalid API KEY"; resp.reset(); resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentLength(error .length()); response.getWriter().write(error); } } }
我尝试通过注入的HttpServletRequest调用getUserPrincipal().getName()获取已认证的Principal,但抛出空指针异常:
java.lang.NullPointerException: Cannot invoke "java.security.Principal.getName()" because the return value of "javax.servlet.http.HttpServletRequest.getUserPrincipal()" is null
已经开启WebSecurity调试,确认过滤器执行顺序正确(自定义Filter在BearerTokenAuthenticationFilter之后),调试日志如下:
************************************************************ Request received for GET '/user?clientId=RDOCUI&clientSecret=dPZprxgxL07Vwgmi73MwR7uLDIioJxpp': org.apache.catalina.connector.RequestFacade@4806674b servletPath:/user pathInfo:null headers: host: localhost:8080 accept-encoding: gzip, deflate accept: */* connection: keep-alive authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzaElqX21OUnZQZUdod1V3ZDdHcUNadU0wNDJibHVjTW5XcTAyT0ZqNW5BIn0.eyJleHAiOjE2ODIxNDc5MjksImlhdCI6MTY4MjE0Nzg2OSwianRpIjoiZjFlMjBkMGMtMjE5MS00ODA1LTkyOGMtOWMzYmExYTdlMDQ4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiIzYzAxZjZkMi1kYzc3LTQ3MzUtOGM1ZC03OWVhMTA0YzI1ZDQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJSRE9DVUkiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIi8qIl0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJjbGllbnRJZCI6IlJET0NVSSIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1yZG9jdWkiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSJ9.ZuiPoV8vItl_XNpzci_8ftYYU-bfhZrr0uSUnSHUq0opfOKKspva9V9MdP_Vj6F50SHKt0WG0EV0dyZohcEsEBVjiPIK9pfrDHgr68hIpbGe1pQ51cCTsKwk2K6vS9WgNWuf3bvS81XAocOSLeQNap4GdiJ6XFjg6IOFalgPesXNvlJYgZ-db_JBe6poGOo2v6MB7MzIPPM9Ks8AqhpDQZPD_RevX03zQb6RNp8gZKwax9-Oqbwq6ptYhnP8ABEIjslb26USfqVcqrKf3DF7QL1BRtRwvwa-wwfrK5mUhmJMrubJon5yBOrNGYJfeqPUMjZ_S0KD6ay41yGgBSOqag user-agent: HTTPie/3.2.1 Security filter chain: [ DisableEncodeUrlFilter WebAsyncManagerIntegrationFilter SecurityContextPersistenceFilter HeaderWriterFilter CorsFilter BearerTokenAuthenticationFilter CommonsHeaderArePresentFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter SessionManagementFilter ExceptionTranslationFilter AuthorizationFilter ] ************************************************************
现在需要解决两个问题:
- 为何Principal为null?
- 如何正确获取已认证用户信息?
1. 为何Principal为null?
你注入的HttpServletRequest是Spring容器初始化时的空代理对象,并非当前处理请求的实例。过滤器是单例Bean,容器启动时就会创建,此时还没有任何HTTP请求进来,注入的HttpServletRequest根本不对应任何实际请求,所以调用getUserPrincipal()自然返回null。
另外,过滤器会被所有请求共享,直接在构造方法中注入请求对象的做法本身就不符合请求生命周期的逻辑——每个请求都有独立的HttpServletRequest实例,单例过滤器无法通过注入绑定到具体请求。
2. 如何正确获取已认证用户信息?
有两种可靠的实现方式:
方式一:使用doFilter方法传入的当前请求对象
过滤器的doFilter方法本身就会传入当前请求的ServletRequest参数,将其强转为HttpServletRequest即可获取当前请求的认证信息:
@Slf4j public class CommonsHeaderArePresentFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest currentRequest = (HttpServletRequest) request; // 获取当前请求的Principal Principal principal = currentRequest.getUserPrincipal(); if (principal != null) { String principalName = principal.getName(); log.info(principalName); } // 验证请求参数逻辑 Optional<String> userRequestParameter = Optional.ofNullable(currentRequest.getParameter("user")); if(userRequestParameter.isPresent()){ chain.doFilter(request, response); }else{ HttpServletResponse resp = (HttpServletResponse) response; String error = "Invalid API KEY"; resp.reset(); resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); resp.setContentLength(error.length()); resp.getWriter().write(error); } } }
方式二:从SecurityContextHolder获取认证信息
Spring Security会将已认证的Authentication对象存入SecurityContextHolder,直接从这里获取更可靠,还能直接拿到JWT的自定义声明:
@Slf4j public class CommonsHeaderArePresentFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 获取当前认证信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { String principalName = authentication.getName(); log.info(principalName); // 如果是JWT令牌,直接获取clientId声明 if (authentication instanceof JwtAuthenticationToken jwtToken) { String clientId = jwtToken.getToken().getClaimAsString("clientId"); HttpServletRequest currentRequest = (HttpServletRequest) request; Optional<String> userParam = Optional.ofNullable(currentRequest.getParameter("user")); // 验证参数和clientId是否匹配 if (!userParam.isPresent() || !userParam.get().equals(clientId)) { HttpServletResponse resp = (HttpServletResponse) response; resp.reset(); resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); resp.getWriter().write("Invalid API KEY or client mismatch"); return; } } } chain.doFilter(request, response); } }
同时需要修改SecurityFilterChain的配置,不再传入HttpServletRequest给过滤器:
// 原代码: // .addFilterAfter(new CommonsHeaderArePresentFilter(this.httpServletRequest), BearerTokenAuthenticationFilter.class) // 修改为: .addFilterAfter(new CommonsHeaderArePresentFilter(), BearerTokenAuthenticationFilter.class)
并去掉过滤器中的HttpServletRequest成员变量和@RequiredArgsConstructor注解。
内容的提问来源于stack exchange,提问作者Jordi

