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

替换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();
    }

}
分析与解决

你的代码存在两个核心问题:

  1. HttpSession获取方式错误
    改造前代码用request.getSession(true),这个方法会在当前没有会话时自动创建新会话;而你现在调用context.getCarrier().getSession(),默认参数是false,没有会话时直接返回null,导致extractUsername方法中session为null,直接返回空字符串。

  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 00:21:01