替换WebMvcTagsProvider为ServerRequestObservationConvention后用户名指标为空
问题
在Spring新版本中,WebMvcTagsProvider已被弃用,我尝试将其替换为ServerRequestObservationConvention,但功能表现不一致。此前使用WebMvcTagsProvider时,我能暴露标识请求发起者用户名的自定义指标,现在采用相同逻辑却获取到null值。请问我哪里操作有误?
改造前代码
@Configuration @EnableWebSecurity @EnableConfigurationProperties public class SecurityConfig extends WebSecurityConfigurerAdapter { private UserSecurityConfig userSecurityConfig; @Autowired public SecurityConfig(UserSecurityConfig userSecurityConfig) { this.userSecurityConfig = userSecurityConfig; } @Bean @Override public UserDetailsService userDetailsService() { PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); userSecurityConfig.getUsers() .stream() .map(user -> User.withUsername(user.getName()) .password(encoder.encode(user.getPass())) .roles(user.getRoles()).build() ) .forEach(manager::createUser); return manager; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().fullyAuthenticated(); http.httpBasic(); http.csrf().disable(); } } @Component public class PrometheusWebTagsProvider implements WebMvcTagsProvider { private static final String SPRING_SECURITY_CONTEXT_ATTRIBUTE = "SPRING_SECURITY_CONTEXT"; @Override public Iterable<Tag> getTags(HttpServletRequest request, HttpServletResponse response, Object handler, Throwable ex) { HttpSession session = request.getSession(true); return Arrays.asList( WebMvcTags.method(request), WebMvcTags.uri(request, response), WebMvcTags.exception(ex), WebMvcTags.status(response), WebMvcTags.outcome(response), Tag.of("username", extractUsername(session)) ); } @Override public Iterable<Tag> getLongRequestTags(HttpServletRequest request, Object handler) { return Arrays.asList(WebMvcTags.method(request), WebMvcTags.uri(request, null)); } private String extractUsername(HttpSession session) { SecurityContext springSecurityContext = (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_ATTRIBUTE); if(springSecurityContext == null) { return ""; } Authentication authentication = springSecurityContext.getAuthentication(); if(authentication == null) { return ""; } return authentication.getName(); } }
改造后代码
@Configuration @EnableWebSecurity @EnableConfigurationProperties public class SecurityConfig { private final UserSecurityConfig userSecurityConfig; @Autowired public SecurityConfig(UserSecurityConfig userSecurityConfig) { this.userSecurityConfig = userSecurityConfig; } @Bean public UserDetailsService userDetailsService() { PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); userSecurityConfig.getUsers() .stream() .map(user -> User.withUsername(user.getName()) .password(encoder.encode(user.getPass())) .roles(user.getRoles()).build() ) .forEach(manager::createUser); return manager; } @Bean public SecurityFilterChain configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth.anyRequest().fullyAuthenticated()) .httpBasic(withDefaults()) .csrf(AbstractHttpConfigurer::disable); return http.build(); } } @Component @Slf4j public class PrometheusWebTagsProvider implements ServerRequestObservationConvention { private static final String SPRING_SECURITY_CONTEXT_ATTRIBUTE = "SPRING_SECURITY_CONTEXT"; @Override public String getName() { return "http.server.requests"; } @Override public String getContextualName(ServerRequestObservationContext context) { return "http " + context.getCarrier().getMethod().toLowerCase(); } @Override @NonNull public KeyValues getLowCardinalityKeyValues(@NonNull ServerRequestObservationContext context) { return KeyValues.of(method(context), status(context), exception(context)).and(additionalTags(context)); } private KeyValues additionalTags(ServerRequestObservationContext context) { KeyValues keyValues = KeyValues.empty(); keyValues = keyValues.and(method(context)); keyValues = keyValues.and(status(context)); keyValues = keyValues.and(exception(context)); keyValues = keyValues.and(KeyValue.of("uri", context.getCarrier().getRequestURI())); keyValues = keyValues.and(KeyValue.of("username", extractUsername(context.getCarrier().getSession()))); keyValues = keyValues.and(KeyValue.of("outcome", outcome(context.getResponse()).getValue())); return keyValues; } private KeyValue method(ServerRequestObservationContext context) { return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod()); } private KeyValue status(ServerRequestObservationContext context) { return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, Integer.toString(context.getResponse().getStatus())); } private KeyValue exception(ServerRequestObservationContext context) { Throwable error = context.getError(); if (error == null) { return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, "none"); } return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, error.getClass().getName()); } private String extractUsername(HttpSession session) { SecurityContext springSecurityContext = (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_ATTRIBUTE); if (springSecurityContext == null) { return ""; } Authentication authentication = springSecurityContext.getAuthentication(); if (authentication == null) { return ""; } return authentication.getName(); } private static Tag outcome(HttpServletResponse response) { Outcome outcome = response != null ? Outcome.forStatus(response.getStatus()) : Outcome.UNKNOWN; return outcome.asTag(); } }
分析与解决
你的代码存在两个核心问题:
HttpSession获取方式错误
改造前代码用request.getSession(true),这个方法会在当前没有会话时自动创建新会话;而你现在调用context.getCarrier().getSession(),默认参数是false,没有会话时直接返回null,导致extractUsername方法中session为null,直接返回空字符串。SecurityContext获取逻辑过时
在使用SecurityFilterChain的Spring Security新版本中,默认不再将SecurityContext存储到HttpSession中,而是通过ThreadLocal来维护当前请求的安全上下文。你还从Session中读取SPRING_SECURITY_CONTEXT_ATTRIBUTE,自然拿不到有效数据。
修正后的代码
修改PrometheusWebTagsProvider中的关键部分:
@Component @Slf4j public class PrometheusWebTagsProvider implements ServerRequestObservationConvention { @Override public String getName() { return "http.server.requests"; } @Override public String getContextualName(ServerRequestObservationContext context) { return "http " + context.getCarrier().getMethod().toLowerCase(); } @Override @NonNull public KeyValues getLowCardinalityKeyValues(@NonNull ServerRequestObservationContext context) { return KeyValues.of(method(context), status(context), exception(context), outcome(context)) .and(KeyValue.of("uri", context.getCarrier().getRequestURI())) .and(KeyValue.of("username", extractUsername())); } private KeyValue method(ServerRequestObservationContext context) { return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod()); } private KeyValue status(ServerRequestObservationContext context) { return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, Integer.toString(Optional.ofNullable(context.getResponse()).map(HttpServletResponse::getStatus).orElse(500))); } private KeyValue exception(ServerRequestObservationContext context) { Throwable error = context.getError(); String exceptionName = error == null ? "none" : error.getClass().getName(); return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, exceptionName); } private KeyValue outcome(ServerRequestObservationContext context) { Outcome outcome = Optional.ofNullable(context.getResponse()) .map(Outcome::forStatus) .orElse(Outcome.UNKNOWN); return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.OUTCOME, outcome.name()); } private String extractUsername() { SecurityContext securityContext = SecurityContextHolder.getContext(); if (securityContext == null) { return ""; } Authentication authentication = securityContext.getAuthentication(); if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) { return ""; } return authentication.getName(); } }
关键修正点
- 移除了从HttpSession获取SecurityContext的逻辑,改用
SecurityContextHolder.getContext()直接获取当前请求的安全上下文 - 简化了标签拼接逻辑,避免重复添加method、status等标签
- 优化了status和outcome的处理,兼容response为null的场景
- 增加了对匿名用户的判断,避免返回"anonymousUser"作为用户名
内容的提问来源于stack exchange,提问作者Denis
相关产品推荐
相关产品推荐

