Spring Boot 2.0/Tomcat 8.5日志中Session用户名属性无法显示问题排查
我来帮你拆解下为什么Tomcat访问日志里的%{username}s总是显示-,以及怎么解决这个问题:
核心原因:容器层与Spring Session的不兼容
你用Redis做Session持久化,大概率是用了Spring Session这类框架对吧?Tomcat的%{xxx}s语法是直接从Tomcat原生内存Session里读取属性,但Spring Session是通过代理HttpSession的方式把数据存在Redis里,Tomcat的Access Log Valve属于容器级别组件,它没法直接读取到Spring Session代理对象里的属性——这就是为什么日志里拿不到你存在Session里的username。
另外还有个细节:你在onAuthenticationSuccess里设置Session属性,这个回调是登录请求处理的最后一步,登录请求本身的日志记录时,username其实还没被写入Session(或者说Tomcat还没拿到这个属性),后续请求也因为Spring Session的代理问题拿不到。
几种可行的解决方案
方案1:改用Spring自定义请求日志(最推荐)
既然用了Spring生态,不如直接在Spring层面做请求日志,这样能轻松拿到Spring Session里的属性,还更灵活:
- 写一个请求过滤器,在请求结束时记录日志:
@Component public class CustomAccessLogFilter extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(CustomAccessLogFilter.class); private static final DateFormat DATE_FORMAT = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z"); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { long start = System.currentTimeMillis(); filterChain.doFilter(request, response); long duration = System.currentTimeMillis() - start; // 直接从Spring Session里拿username,拿不到就显示- String username = Optional.ofNullable((String) request.getSession().getAttribute("username")) .orElse("-"); // 构造和Tomcat日志格式一致的内容 String logLine = String.format( "%s %s %s [%s] \"%s\" %d %d %dms \"%s\"", request.getRemoteAddr(), "-", // %l 通常都是-,因为很少用identd服务 username, DATE_FORMAT.format(new Date()), request.getMethod() + " " + request.getRequestURI() + " " + request.getProtocol(), response.getStatus(), response.getContentLength() == -1 ? 0 : response.getContentLength(), duration, request.getHeader("User-Agent") ); logger.info(logLine); } }
- 关闭Tomcat自带的Access Log:
server.tomcat.accesslog.enabled=false
方案2:用%u替代%{username}s(最简单的容器级方案)
Tomcat的%u代表远程认证用户,Spring Security在认证成功后会自动把当前用户名设置到request.getRemoteUser()里,所以你可以直接用这个占位符替代Session属性:
修改你的Tomcat日志配置:
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %T %{User-Agent}i
如果你的场景里request.getRemoteUser()没自动返回用户名,也可以在onAuthenticationSuccess里手动设置:
@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String username = merchandiserRepository.findByUsername(authentication.getName()).getUsername(); request.getSession(true).setAttribute("username", username); // 手动设置RemoteUser,确保Tomcat能拿到 request.setRemoteUser(username); logger.warn("Successfully logged in: " + username); response.sendRedirect("/page/1"); }
方案3:调整Spring Session同步时机(不推荐,复杂且易出问题)
如果你非要坚持用%{username}s,可以尝试配置Spring Session在修改Session属性时立即同步到Redis,同时确保Tomcat的Access Log Valve在请求完全处理完成后再记录日志。但这种方式需要调整Spring Session的flush-mode为IMMEDIATE,还要修改Tomcat Valve的执行顺序,步骤繁琐且容易引发性能问题,不推荐。
总结
优先选方案1,完全适配Spring生态,后续扩展日志格式也方便;如果不想改日志体系,方案2的%u替换是最省心的办法,能直接复用Tomcat的Access Log。
内容的提问来源于stack exchange,提问作者ROZZ

