Tomcat 会话管理
Session Management
概述
会话管理是Web应用的核心功能,特别在集群环境中。本文详细介绍Tomcat的会话管理机制、持久化存储、集群会话复制和会话优化技术。
1. 会话管理基础
1.1 会话管理器类型
Tomcat会话管理器
├── StandardManager
│ ├── 内存存储
│ └── 单机应用
├── PersistentManager
│ ├── 文件存储
│ ├── 数据库存储
│ └── 持久化会话
├── DeltaManager
│ ├── 集群复制
│ └── 全节点同步
└── BackupManager
├── 备份复制
└── 指定节点备份
1.2 基础会话配置
<!-- server.xml - 基础会话管理器 -->
<Context path="/myapp" docBase="myapp.war">
<!-- 标准会话管理器 -->
<Manager className="org.apache.catalina.session.StandardManager"
maxActiveSessions="1000"
sessionIdLength="16"
randomClass="java.security.SecureRandom"
algorithm="SHA1PRNG"
entropy="/dev/urandom"
maxInactiveInterval="1800"
processExpiresFrequency="6"
secureRandomClass="java.security.SecureRandom"
secureRandomAlgorithm="SHA1PRNG" />
</Context>
2. 持久化会话管理
2.1 文件存储配置
<!-- 文件持久化会话管理器 -->
<Context path="/myapp" docBase="myapp.war">
<Manager className="org.apache.catalina.session.PersistentManager"
saveOnRestart="true"
maxActiveSession="1000"
minIdleSwap="10"
maxIdleSwap="60"
maxIdleBackup="30">
<!-- 文件存储 -->
<Store className="org.apache.catalina.session.FileStore"
directory="/opt/tomcat/sessions"
checkInterval="60" />
</Manager>
</Context>
2.2 数据库存储配置
<!-- 数据库持久化会话管理器 -->
<Context path="/myapp" docBase="myapp.war">
<!-- 数据源配置 -->
<Resource name="jdbc/SessionDB"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/sessions"
username="sessionuser"
password="sessionpass"
maxTotal="20"
maxIdle="10" />
<Manager className="org.apache.catalina.session.PersistentManager"
saveOnRestart="true"
maxActiveSession="2000"
minIdleSwap="15"
maxIdleSwap="120">
<!-- 数据库存储 -->
<Store className="org.apache.catalina.session.JDBCStore"
dataSourceName="jdbc/SessionDB"
sessionTable="tomcat_sessions"
sessionAppCol="app_name"
sessionIdCol="session_id"
sessionDataCol="session_data"
sessionValidCol="valid"
sessionMaxInactiveCol="max_inactive"
sessionLastAccessedCol="last_access"
checkInterval="60" />
</Manager>
</Context>
数据库表结构:
-- 会话存储表
CREATE TABLE tomcat_sessions (
session_id VARCHAR(100) NOT NULL PRIMARY KEY,
app_name VARCHAR(100) NOT NULL,
session_data LONGBLOB,
valid CHAR(1) NOT NULL,
max_inactive INT NOT NULL,
last_access BIGINT NOT NULL,
INDEX idx_app_name (app_name),
INDEX idx_last_access (last_access)
);
-- 清理过期会话存储过程
DELIMITER //
CREATE PROCEDURE CleanExpiredSessions()
BEGIN
DELETE FROM tomcat_sessions
WHERE (UNIX_TIMESTAMP() * 1000 - last_access) > (max_inactive * 1000);
END //
DELIMITER ;
-- 设置定时清理
CREATE EVENT IF NOT EXISTS clean_sessions
ON SCHEDULE EVERY 1 HOUR
DO CALL CleanExpiredSessions();
3. Redis会话存储
3.1 Redis Manager配置
<!-- 自定义Redis会话管理器 -->
<Context path="/myapp" docBase="myapp.war">
<Manager className="com.example.session.RedisSessionManager"
host="redis.example.com"
port="6379"
password="redispass"
database="0"
maxTotal="50"
maxIdle="20"
timeout="5000"
sessionTimeout="1800"
keyPrefix="tomcat:session:" />
</Context>
3.2 Redis会话管理器实现
// RedisSessionManager.java
package com.example.session;
import org.apache.catalina.session.ManagerBase;
import org.apache.catalina.Session;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Jedis;
import java.io.*;
import java.util.Base64;
public class RedisSessionManager extends ManagerBase {
private String host = "localhost";
private int port = 6379;
private String password;
private int database = 0;
private String keyPrefix = "tomcat:session:";
private JedisPool jedisPool;
@Override
public void startInternal() throws LifecycleException {
super.startInternal();
initializeRedisPool();
}
@Override
public void stopInternal() throws LifecycleException {
if (jedisPool != null) {
jedisPool.close();
}
super.stopInternal();
}
private void initializeRedisPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50);
config.setMaxIdle(20);
config.setTestOnBorrow(true);
if (password != null && !password.isEmpty()) {
jedisPool = new JedisPool(config, host, port, 5000, password, database);
} else {
jedisPool = new JedisPool(config, host, port, 5000, null, database);
}
}
@Override
public Session findSession(String id) throws IOException {
if (id == null) return null;
try (Jedis jedis = jedisPool.getResource()) {
String sessionData = jedis.get(keyPrefix + id);
if (sessionData != null) {
return deserializeSession(sessionData);
}
} catch (Exception e) {
getLogger().error("Failed to load session from Redis: " + id, e);
}
return null;
}
@Override
public void add(Session session) {
try (Jedis jedis = jedisPool.getResource()) {
String sessionData = serializeSession(session);
String key = keyPrefix + session.getId();
jedis.setex(key, getMaxInactiveInterval(), sessionData);
sessions.put(session.getId(), session);
} catch (Exception e) {
getLogger().error("Failed to save session to Redis: " + session.getId(), e);
}
}
@Override
public void remove(Session session, boolean update) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.del(keyPrefix + session.getId());
sessions.remove(session.getId());
} catch (Exception e) {
getLogger().error("Failed to remove session from Redis: " + session.getId(), e);
}
}
private String serializeSession(Session session) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(session);
oos.close();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
private Session deserializeSession(String sessionData) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(sessionData);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
Session session = (Session) ois.readObject();
ois.close();
return session;
}
// Getter和Setter方法
public void setHost(String host) { this.host = host; }
public void setPort(int port) { this.port = port; }
public void setPassword(String password) { this.password = password; }
public void setDatabase(int database) { this.database = database; }
public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; }
}
4. 集群会话复制
4.1 DeltaManager配置
<!-- 集群环境下的会话复制 -->
<Engine name="Catalina" defaultHost="localhost" jvmRoute="node1">
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
<!-- Delta会话管理器 -->
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"
maxInactiveInterval="1800"
stateTransferTimeout="60"
sendAllSessions="true"
sendAllSessionsSize="1000"
sendAllSessionsWaitTime="2000" />
<!-- 集群通信配置 -->
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<!-- Channel配置... -->
</Channel>
<!-- 会话复制阀门 -->
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt" />
</Cluster>
</Engine>
4.2 BackupManager配置
<!-- 备份会话管理器 - 只复制到指定节点 -->
<Manager className="org.apache.catalina.ha.session.BackupManager"
mapSendOptions="6"
notifyListenersOnReplication="true"
rpcTimeout="15000" />
5. 会话安全配置
5.1 安全会话配置
<!-- web.xml - 会话安全配置 -->
<web-app>
<session-config>
<session-timeout>30</session-timeout>
<!-- Cookie配置 -->
<cookie-config>
<name>JSESSIONID</name>
<path>/</path>
<comment>Session Cookie</comment>
<http-only>true</http-only>
<secure>true</secure>
<max-age>1800</max-age>
<same-site>Strict</same-site>
</cookie-config>
<!-- 会话跟踪模式 -->
<tracking-mode>COOKIE</tracking-mode>
</session-config>
</web-app>
5.2 会话安全过滤器
// SessionSecurityFilter.java
package com.example.session;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
public class SessionSecurityFilter implements Filter {
private static final ConcurrentHashMap<String, SessionInfo> sessionTracker = new ConcurrentHashMap<>();
private int maxSessionsPerUser = 3;
private int sessionTimeout = 1800; // 30分钟
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpSession session = httpRequest.getSession(false);
if (session != null) {
// 检查会话固化攻击
if (detectSessionFixation(httpRequest, session)) {
session.invalidate();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session security violation");
return;
}
// 检查并发会话
String username = (String) session.getAttribute("username");
if (username != null && !checkConcurrentSessions(username, session.getId())) {
session.invalidate();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Too many concurrent sessions");
return;
}
// 更新会话信息
updateSessionInfo(session, httpRequest);
}
chain.doFilter(request, response);
}
private boolean detectSessionFixation(HttpServletRequest request, HttpSession session) {
String currentIP = request.getRemoteAddr();
String currentUserAgent = request.getHeader("User-Agent");
String sessionIP = (String) session.getAttribute("session.ip");
String sessionUserAgent = (String) session.getAttribute("session.user.agent");
if (sessionIP == null || sessionUserAgent == null) {
session.setAttribute("session.ip", currentIP);
session.setAttribute("session.user.agent", currentUserAgent);
return false;
}
return !currentIP.equals(sessionIP) || !currentUserAgent.equals(sessionUserAgent);
}
private boolean checkConcurrentSessions(String username, String sessionId) {
SessionInfo info = sessionTracker.get(username);
if (info == null) {
info = new SessionInfo();
sessionTracker.put(username, info);
}
// 清理过期会话
info.cleanExpiredSessions();
// 检查会话数量
if (info.getActiveSessionCount() >= maxSessionsPerUser &&
!info.hasSession(sessionId)) {
return false;
}
info.addSession(sessionId);
return true;
}
private void updateSessionInfo(HttpSession session, HttpServletRequest request) {
session.setAttribute("session.last.access", System.currentTimeMillis());
session.setAttribute("session.request.count",
((Integer) session.getAttribute("session.request.count")) + 1);
}
private static class SessionInfo {
private final ConcurrentHashMap<String, Long> sessions = new ConcurrentHashMap<>();
public void addSession(String sessionId) {
sessions.put(sessionId, System.currentTimeMillis());
}
public boolean hasSession(String sessionId) {
return sessions.containsKey(sessionId);
}
public int getActiveSessionCount() {
return sessions.size();
}
public void cleanExpiredSessions() {
long now = System.currentTimeMillis();
sessions.entrySet().removeIf(entry ->
(now - entry.getValue()) > (1800 * 1000)); // 30分钟超时
}
}
}
6. 会话监控与管理
6.1 会话监控工具
// SessionMonitor.java
package com.example.session;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.util.Set;
public class SessionMonitor {
private MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
public void monitorSessions() throws Exception {
Set<ObjectName> managers = mbs.queryNames(
new ObjectName("Catalina:type=Manager,*"), null
);
System.out.println("=== 会话监控报告 ===");
for (ObjectName manager : managers) {
String context = manager.getKeyProperty("path");
if (context.isEmpty()) context = "/";
Integer activeSessions = (Integer) mbs.getAttribute(manager, "activeSessions");
Integer maxActiveSessions = (Integer) mbs.getAttribute(manager, "maxActiveSessions");
Long sessionCounter = (Long) mbs.getAttribute(manager, "sessionCounter");
Long expiredSessions = (Long) mbs.getAttribute(manager, "expiredSessions");
Integer sessionMaxAliveTime = (Integer) mbs.getAttribute(manager, "sessionMaxAliveTime");
Integer sessionAverageAliveTime = (Integer) mbs.getAttribute(manager, "sessionAverageAliveTime");
Integer rejectedSessions = (Integer) mbs.getAttribute(manager, "rejectedSessions");
System.out.println("应用路径: " + context);
System.out.println(" 当前活跃会话: " + activeSessions);
System.out.println(" 最大活跃会话: " + maxActiveSessions);
System.out.println(" 会话总数: " + sessionCounter);
System.out.println(" 过期会话: " + expiredSessions);
System.out.println(" 最长存活时间: " + sessionMaxAliveTime + "秒");
System.out.println(" 平均存活时间: " + sessionAverageAliveTime + "秒");
System.out.println(" 拒绝会话数: " + rejectedSessions);
// 计算会话使用率
if (maxActiveSessions > 0) {
double usage = (activeSessions * 100.0) / maxActiveSessions;
System.out.println(" 会话使用率: " + String.format("%.1f%%", usage));
}
System.out.println();
}
}
public void cleanupExpiredSessions() throws Exception {
Set<ObjectName> managers = mbs.queryNames(
new ObjectName("Catalina:type=Manager,*"), null
);
for (ObjectName manager : managers) {
try {
mbs.invoke(manager, "processExpires", null, null);
System.out.println("已清理过期会话: " + manager.getKeyProperty("path"));
} catch (Exception e) {
System.err.println("清理会话失败: " + e.getMessage());
}
}
}
}
6.2 会话管理脚本
#!/bin/bash
# session-manager.sh
TOMCAT_PID=$(jps -l | grep Bootstrap | awk '{print $1}')
show_session_stats() {
if [ -z "$TOMCAT_PID" ]; then
echo "Tomcat进程未找到"
return 1
fi
echo "=== 会话统计信息 ==="
# 使用JMX获取会话信息
java -cp "$CATALINA_HOME/lib/*" \
-Dcom.sun.management.jmxremote \
SessionMonitor 2>/dev/null || {
echo "无法获取JMX数据,使用替代方法..."
# 通过manager应用获取信息
curl -s -u admin:admin123 \
"http://localhost:8080/manager/text/sessions?path=/myapp" | \
while read line; do
echo " $line"
done
}
}
clean_sessions() {
echo "清理过期会话..."
curl -s -u admin:admin123 \
"http://localhost:8080/manager/text/expire?path=/myapp&idle=1800"
}
monitor_sessions() {
echo "开始监控会话..."
while true; do
clear
echo "会话监控 - $(date)"
show_session_stats
sleep 30
done
}
backup_sessions() {
echo "备份会话数据..."
# 如果使用文件存储
if [ -d "/opt/tomcat/sessions" ]; then
tar -czf "/tmp/sessions_backup_$(date +%Y%m%d_%H%M%S).tar.gz" \
/opt/tomcat/sessions/
echo "会话文件已备份"
fi
# 如果使用数据库存储
if command -v mysqldump &> /dev/null; then
mysqldump -u sessionuser -psessionpass sessions tomcat_sessions > \
"/tmp/sessions_$(date +%Y%m%d_%H%M%S).sql"
echo "会话数据库已备份"
fi
}
case "$1" in
"stats") show_session_stats ;;
"clean") clean_sessions ;;
"monitor") monitor_sessions ;;
"backup") backup_sessions ;;
*)
echo "用法: $0 {stats|clean|monitor|backup}"
echo " stats - 显示会话统计"
echo " clean - 清理过期会话"
echo " monitor - 实时监控会话"
echo " backup - 备份会话数据"
;;
esac
7. 会话优化最佳实践
7.1 会话优化配置
<!-- 优化的会话配置 -->
<Context path="/myapp" docBase="myapp.war">
<Manager className="org.apache.catalina.session.StandardManager"
maxActiveSessions="5000"
sessionIdLength="32"
randomClass="java.security.SecureRandom"
maxInactiveInterval="1800"
processExpiresFrequency="6"
pathname="" <!-- 禁用会话持久化到文件 -->
randomFile="/dev/urandom" />
<!-- 会话Cookie处理器 -->
<CookieProcessor className="org.apache.tomcat.util.http.Rfc6265CookieProcessor"
sameSiteCookies="strict" />
</Context>
7.2 会话性能监控
// SessionPerformanceMonitor.java
package com.example.session;
public class SessionPerformanceMonitor {
public void analyzeSessionPerformance() {
System.out.println("=== 会话性能分析 ===");
try {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName managerName = new ObjectName("Catalina:type=Manager,path=/myapp");
// 关键性能指标
Integer activeSessions = (Integer) server.getAttribute(managerName, "activeSessions");
Long sessionCounter = (Long) server.getAttribute(managerName, "sessionCounter");
Long expiredSessions = (Long) server.getAttribute(managerName, "expiredSessions");
Integer avgAliveTime = (Integer) server.getAttribute(managerName, "sessionAverageAliveTime");
Integer maxAliveTime = (Integer) server.getAttribute(managerName, "sessionMaxAliveTime");
// 计算性能指标
double sessionTurnover = expiredSessions.doubleValue() / sessionCounter.doubleValue();
double currentLoad = activeSessions.doubleValue() / 5000.0; // 假设最大5000会话
System.out.println("当前活跃会话: " + activeSessions);
System.out.println("会话周转率: " + String.format("%.2f%%", sessionTurnover * 100));
System.out.println("当前负载: " + String.format("%.1f%%", currentLoad * 100));
System.out.println("平均存活时间: " + avgAliveTime + "秒");
System.out.println("最长存活时间: " + maxAliveTime + "秒");
// 性能建议
if (currentLoad > 0.8) {
System.out.println("建议: 会话负载过高,考虑优化或扩容");
}
if (avgAliveTime < 300) {
System.out.println("建议: 会话存活时间过短,检查应用逻辑");
}
if (sessionTurnover > 0.5) {
System.out.println("建议: 会话周转率过高,检查会话配置");
}
} catch (Exception e) {
System.err.println("性能分析失败: " + e.getMessage());
}
}
}
小结
通过本文学习,你应该掌握:
- Tomcat会话管理器的类型和配置
- 持久化会话存储的实现方法
- Redis等外部存储的集成技术
- 集群环境下的会话复制配置
- 会话安全防护措施
- 会话监控和性能优化技术
- 会话管理的最佳实践
下一篇文章将介绍Tomcat日志配置与管理。