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

Spring Java MVC中从SecurityContextHolder获取用户邮箱及Log4j集成问题

解决Spring MVC中Log4j获取登录用户邮箱并输出到日志的问题

首先,你遇到的核心问题是Spring Security默认的UserDetails实现(org.springframework.security.core.userdetails.User)并不包含邮箱字段,所以直接从SecurityContextHolder拿到的用户对象自然没有获取邮箱的方法。咱们一步步来解决:

1. 自定义包含邮箱的UserDetails实现

先创建一个自己的用户类,继承默认的User类(或者直接实现UserDetails接口),把邮箱字段加进去:

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

// 继承默认User类,添加邮箱字段
public class CustomUserDetails extends User {
    private final String email;

    public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, String email) {
        super(username, password, authorities);
        this.email = email;
    }

    // 提供邮箱的getter方法
    public String getEmail() {
        return email;
    }
}

如果不想继承,也可以直接实现UserDetails接口自己实现所有方法,但继承默认User会更省事。

2. 在UserDetailsService中加载用户数据时填充邮箱

接下来,你的UserDetailsService实现类里,从数据库查询用户信息时要把邮箱一起查出来,然后创建CustomUserDetails对象返回:

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    // 注入你的用户DAO/Repository
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查询用户,包含邮箱
        UserEntity user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));

        // 创建自定义的UserDetails对象,传入邮箱
        return new CustomUserDetails(
                user.getUsername(),
                user.getPassword(),
                user.getAuthorities(), // 这里要转换为GrantedAuthority集合
                user.getEmail()
        );
    }
}

3. 将邮箱存入Log4j的MDC(映射诊断上下文)

Log4j的MDC可以让你在整个请求生命周期中存储全局变量,方便在日志格式里引用。我们可以用Spring Security过滤器来在请求开始时把邮箱放入MDC:

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.ThreadContext; // Log4j2用ThreadContext,Log4j1用org.apache.log4j.MDC

import java.io.IOException;

public class UserEmailMdcFilter extends OncePerRequestFilter {

    private static final String MDC_EMAIL_KEY = "userEmail";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null && authentication.isAuthenticated() && authentication.getPrincipal() instanceof CustomUserDetails) {
                // 强制转换为自定义的UserDetails
                CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
                // 把邮箱放入MDC
                ThreadContext.put(MDC_EMAIL_KEY, userDetails.getEmail());
            } else {
                // 未登录时设置默认值
                ThreadContext.put(MDC_EMAIL_KEY, "anonymous");
            }
            filterChain.doFilter(request, response);
        } finally {
            // 请求结束后清理MDC,避免内存泄漏
            ThreadContext.remove(MDC_EMAIL_KEY);
        }
    }
}

然后把这个过滤器添加到Spring Security的过滤器链中:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // ... 你的其他Spring Security配置
                .addFilterBefore(new UserEmailMdcFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

4. 在log4j.properties中配置日志格式

现在MDC里已经有userEmail这个键了,直接在日志格式里引用即可:

# Log4j 1.x 配置示例
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p [%X{userEmail}] %c{1}:%L - %m%n

# Log4j 2.x 配置示例(如果用的是Log4j2)
appender.stdout.layout.pattern=%d{yyyy-MM-dd HH:mm:ss} %-5p [%X{userEmail}] %c{1}:%L - %m%n

这里的%X{userEmail}就是引用MDC中存储的邮箱值。

额外注意事项

  • 如果你的项目用的是Log4j 1.x,MDC类是org.apache.log4j.MDC,而Log4j2用的是org.apache.logging.log4j.ThreadContext,别搞混了。
  • 强制转换CustomUserDetails时加了类型判断,避免出现ClassCastException,这是个好习惯。
  • 未登录的请求,MDC里的邮箱会显示为"anonymous",你可以根据需求调整这个默认值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 08:50:14