Tomcat-session的实现:线程安全与管理(1),带你玩转自定义view系列

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注网络安全)
img

正文

return (null); // Sessions are not supported
}
if (requestedSessionId != null) {
try {
// 如果不是第一次请求,则会带上服务返回的 sessionId, 就会主动查找原来的session
// 从 sessions 中查找即可
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
session = null;
}
if ((session != null) && !session.isValid()) {
session = null;
}
// 后续请求,每次请求都会更新有效时间
if (session != null) {
session.access();
return (session);
}
}

// Create a new session if requested and the response is not committed
// 主动请求session时,才会继续后续逻辑
if (!create) {
return (null);
}
if (response != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)
&& response.getResponse().isCommitted()) {
throw new IllegalStateException(
sm.getString(“coyoteRequest.sessionCreateCommitted”));
}

// Re-use session IDs provided by the client in very limited
// circumstances.
String sessionId = getRequestedSessionId();
if (requestedSessionSSL) {
// If the session ID has been obtained from the SSL handshake then
// use it.
} else if ((“/”.equals(context.getSessionCookiePath())
&& isRequestedSessionIdFromCookie())) {
/* This is the common(ish) use case: using the same session ID with

  • multiple web applications on the same host. Typically this is
  • used by Portlet implementations. It only works if sessions are
  • tracked via cookies. The cookie must have a path of “/” else it
  • won’t be provided for requests to all web applications.
  • Any session ID provided by the client should be for a session
  • that already exists somewhere on the host. Check if the context
  • is configured for this to be confirmed.
    */
    if (context.getValidateClientProvidedNewSessionId()) {
    boolean found = false;
    for (Container container : getHost().findChildren()) {
    Manager m = ((Context) container).getManager();
    if (m != null) {
    try {
    if (m.findSession(sessionId) != null) {
    found = true;
    break;
    }
    } catch (IOException e) {
    // Ignore. Problems with this manager will be
    // handled elsewhere.
    }
    }
    }
    if (!found) {
    sessionId = null;
    }
    }
    } else {
    // 当session无效时,需要将原来的seesionId置空,删除并新创建一个使用
    sessionId = null;
    }
    // 创建session, StandardManager -> ManagerBase
    session = manager.createSession(sessionId);

// Creating a new session cookie based on that session
if (session != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)) {
// 创建cookie信息,与session对应
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
// 添加到response中,在响应结果一起返回给客户端
response.addSessionCookieInternal(cookie);
}

if (session == null) {
return null;
}
// 每次请求session时,必然刷新激活时间,以便判定会话是否超时
session.access();
return session;
}

从上面我们可以看到,session的流程大概是这样的:

1. 先查找是否有session信息存在,如果有则判断是否失效;
2. 如果不存在session或已失效,则使用一个新的sessionId(非必须)创建一个session实例;
3. session创建成功,则将sessionId写入到cookie信息中,以便客户端后续使用;
4. 每次请求完session,必定刷新下访问时间以续期;

session的管理主要有两种实现方式,类图如下:

我们先主要以基于内存的实现来理解下session的管理过程。实际上StandardManager基本就依托于 ManagerBase 就实现了Session管理功能,下面我们来看一下其创建session如何?

// org.apache.catalina.session.ManagerBase#createSession
@Override
public Session createSession(String sessionId) {
// 首先来个安全限制,允许同时存在多少会话
// 这个会话实际上代表的是一段时间的有效性,并非真正的用户有效使用在线,所以该值一般要求比预计的数量大些才好
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString(“managerBase.createSession.ise”),
maxActiveSessions);
}

// Recycle or create a Session instance
// 创建空的session 容器 return new StandardSession(this);
Session session = createEmptySession();

// Initialize the properties of the new session and return it
// 默认30分钟有效期
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
// sessionId 为空时,生成一个,随机id
id = generateSessionId();
}
// 设置sessionId, 注意此处不仅仅是set这么简单,其同时会将自身session注册到全局session管理器中.如下文
session.setId(id);
sessionCounter++;

SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
// LinkedList, 添加一个,删除一个?
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return (session);

}
// org.apache.catalina.session.StandardSession#setId
/**

  • Set the session identifier for this session.
  • @param id The new session identifier
    */
    @Override
    public void setId(String id) {
    setId(id, true);
    }
    @Override
    public void setId(String id, boolean notify) {
    // 如果原来的id不为空,则先删除原有的
    if ((this.id != null) && (manager != null))
    manager.remove(this);

this.id = id;
// 再将自身会话注册到 manager 中,即 sessions 中
if (manager != null)
manager.add(this);
// 通知监听者,这是框架该做好的事(扩展点),不过不是本文的方向,忽略
if (notify) {
tellNew();
}
}
// org.apache.catalina.session.ManagerBase#add
@Override
public void add(Session session) {
// 取出 sessionId, 添加到 sessions 容器,统一管理
sessions.put(session.getIdInternal(), session);
int size = getActiveSessions();
// 刷新最大活跃数,使用双重锁优化更新该值
if( size > maxActive ) {
synchronized(maxActiveUpdateLock) {
if( size > maxActive ) {
maxActive = size;
}
}
}
}
// 查找session也是异常简单,只管从 ConcurrentHashMap 中查找即可
// org.apache.catalina.session.ManagerBase#findSession
@Override
public Session findSession(String id) throws IOException {
if (id == null) {
return null;
}
return sessions.get(id);
}

创建好session后,需要进行随时的维护:我们看下tomcat是如何刷新访问时间的?可能比预想的简单,其仅是更新一个访问时间字段,再无其他。

// org.apache.catalina.session.StandardSession#access
/**

  • Update the accessed time information for this session. This method
  • should be called by the context when a request comes in for a particular
  • session, even if the application does not reference it.
    */
    @Override
    public void access() {
    // 更新访问时间
    this.thisAccessedTime = System.currentTimeMillis();
    // 访问次数统计,默认不启用
    if (ACTIVITY_CHECK) {
    accessCount.incrementAndGet();
    }

}

最后,还需要看下 HttpSession 是如何被包装返回的?

// org.apache.catalina.session.StandardSession#getSession
/**

  • Return the HttpSession for which this object
  • is the facade.
    */
    @Override
    public HttpSession getSession() {

if (facade == null){
if (SecurityUtil.isPackageProtectionEnabled()){
final StandardSession fsession = this;
facade = AccessController.doPrivileged(
new PrivilegedAction(){
@Override
public StandardSessionFacade run(){
return new StandardSessionFacade(fsession);
}
});
} else {
// 直接使用 StandardSessionFacade 包装即可
facade = new StandardSessionFacade(this);
}
}
return (facade);

}

再最后,要说明的是,整个sessions的管理使用一个 ConcurrentHashMap 来存放全局会话信息,sessionId->session实例。

对于同一次http请求中,该session会被存储在当前的Request栈org.apache.catalina.connector.Request#session字段中,从而无需每次深入获取。每个请求进来后,会将session保存在当前的request信息中。

3. 过期session清理?

会话不可能不过期,不过期的也不叫会话了。

会话过期的触发时机主要有三个:1. 每次进行会话调用时,会主动有效性isValid()验证,此时如果发现过期可以主动清理: 2. 后台定时任务触发清理; 3. 启动或停止应用的时候清理;(这对于非内存式的存储会更有用些)

// case1. 请求时验证,如前面所述
// org.apache.catalina.connector.Request#doGetSession
protected Session doGetSession(boolean create) {

// Return the current session if it exists and is valid
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
return (session);
}

}

// case2. 后台定时任务清理
// org.apache.catalina.session.ManagerBase#backgroundProcess
@Override
public void backgroundProcess() {
// 并非每次定时任务到达时都会进行清理,而是要根据其清理频率设置来运行
// 默认是 6
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
/**

  • Invalidate all sessions that have expired.
    */
    public void processExpires() {

long timeNow = System.currentTimeMillis();
// 找出所有的sessions, 转化为数组遍历
Session sessions[] = findSessions();
int expireHere = 0 ;

if(log.isDebugEnabled())
log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
for (int i = 0; i < sessions.length; i++) {
// 事实上后台任务也是调用 isValid() 方法 进行过期任务清理的
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
if(log.isDebugEnabled())
log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
processingTime += ( timeEnd - timeNow );

}

//case3. start/stop 时触发过期清理(生命周期事件)
// org.apache.catalina.session.StandardManager#startInternal
/**

  • Start this component and implement the requirements
  • of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
  • @exception LifecycleException if this component detects a fatal error
  • that prevents this component from being used
    */
    @Override
    protected synchronized void startInternal() throws LifecycleException {

super.startInternal();

// Load unloaded sessions, if any
try {
// doLoad() 调用
load();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString(“standardManager.managerLoad”), t);
}

setState(LifecycleState.STARTING);
}

/**

  • Load any currently active sessions that were previously unloaded
  • to the appropriate persistence mechanism, if any. If persistence is not
  • supported, this method returns without doing anything.
  • @exception ClassNotFoundException if a serialized class cannot be
  • found during the reload
  • @exception IOException if an input/output error occurs
    */
    protected void doLoad() throws ClassNotFoundException, IOException {
    if (log.isDebugEnabled()) {
    log.debug(“Start: Loading persisted sessions”);
    }

// Initialize our internal data structures
sessions.clear();

// Open an input stream to the specified pathname, if any
File file = file();
if (file == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug(sm.getString(“standardManager.loading”, pathname));
}
Loader loader = null;
ClassLoader classLoader = null;
Log logger = null;
try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());
BufferedInputStream bis = new BufferedInputStream(fis)) {
Context c = getContext();
loader = c.getLoader();
logger = c.getLogger();
if (loader != null) {
classLoader = loader.getClassLoader();
}
if (classLoader == null) {
classLoader = getClass().getClassLoader();
}

// Load the previously unloaded active sessions
synchronized (sessions) {
try (ObjectInputStream ois = new CustomObjectInputStream(bis, classLoader, logger,
getSessionAttributeValueClassNamePattern(),
getWarnOnSessionAttributeFilterFailure())) {
Integer count = (Integer) ois.readObject();
int n = count.intValue();
if (log.isDebugEnabled())
log.debug(“Loading " + n + " persisted sessions”);
for (int i = 0; i < n; i++) {
StandardSession session = getNewSession();
session.readObjectData(ois);
session.setManager(this);
sessions.put(session.getIdInternal(), session);
session.activate();
if (!session.isValidInternal()) {
// If session is already invalid,
// expire session to prevent memory leak.
// 主动调用 expire
session.setValid(true);
session.expire();
}
sessionCounter++;
}
} finally {
// Delete the persistent storage file
if (file.exists()) {
file.delete();
}
}
}
} catch (FileNotFoundException e) {
if (log.isDebugEnabled()) {
log.debug(“No persisted data file found”);
}
return;
}

if (log.isDebugEnabled()) {
log.debug(“Finish: Loading persisted sessions”);
}
}
// stopInternal() 事件到达时清理 sessions
/**

  • Save any currently active sessions in the appropriate persistence
  • mechanism, if any. If persistence is not supported, this method
  • returns without doing anything.
  • @exception IOException if an input/output error occurs
    */
    protected void doUnload() throws IOException {

if (log.isDebugEnabled())
log.debug(sm.getString(“standardManager.unloading.debug”));

if (sessions.isEmpty()) {
log.debug(sm.getString(“standardManager.unloading.nosessions”));
return; // nothing to do
}

// Open an output stream to the specified pathname, if any
File file = file();
if (file == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug(sm.getString(“standardManager.unloading”, pathname));
}

// Keep a note of sessions that are expired
ArrayList list = new ArrayList<>();

try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos)) {

synchronized (sessions) {
if (log.isDebugEnabled()) {
log.debug(“Unloading " + sessions.size() + " sessions”);
}
// Write the number of active sessions, followed by the details
oos.writeObject(Integer.valueOf(sessions.size()));
for (Session s : sessions.values()) {
StandardSession session = (StandardSession) s;
list.add(session);
session.passivate();
session.writeObjectData(oos);
}
}
}

// Expire all the sessions we just wrote
// 将所有session失效,实际上应用即将关闭,失不失效的应该也无所谓了
if (log.isDebugEnabled()) {
log.debug(“Expiring " + list.size() + " persisted sessions”);
}
for (StandardSession session : list) {
try {
session.expire(false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
session.recycle();
}
}

if (log.isDebugEnabled()) {
log.debug(“Unloading complete”);
}
}

接下来我们看下具体如何清理过期的会话?实际应该就是一个remove的事。

// org.apache.catalina.session.StandardSession#isValid
/**

  • Return the isValid flag for this session.
    */
    @Override
    public boolean isValid() {

if (!this.isValid) {
return false;
}

if (this.expiring) {
return true;
}

if (ACTIVITY_CHECK && accessCount.get() > 0) {
return true;
}
// 超过有效期,主动触发清理
if (maxInactiveInterval > 0) {
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
if (timeIdle >= maxInactiveInterval) {
expire(true);
}
}

return this.isValid;
}

// org.apache.catalina.session.StandardSession#expire(boolean)
/**

  • Perform the internal processing required to invalidate this session,
  • without triggering an exception if the session has already expired.
  • @param notify Should we notify listeners about the demise of
  • this session?
    */
    public void expire(boolean notify) {

// Check to see if session has already been invalidated.
// Do not check expiring at this point as expire should not return until
// isValid is false
if (!isValid)
return;
// 上锁保证线程安全
synchronized (this) {
// Check again, now we are inside the sync so this code only runs once
// Double check locking - isValid needs to be volatile
// The check of expiring is to ensure that an infinite loop is not
// entered as per bug 56339
if (expiring || !isValid)
return;

if (manager == null)
return;

// Mark this session as “being expired”
expiring = true;

// Notify interested application event listeners
// FIXME - Assumes we call listeners in reverse order
Context context = manager.getContext();

// The call to expire() may not have been triggered by the webapp.
// Make sure the webapp’s class loader is set when calling the
// listeners
if (notify) {
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null && listeners.length > 0) {
HttpSessionEvent event =
new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
int j = (listeners.length - 1) - i;
if (!(listeners[j] instanceof HttpSessionListener))
continue;
HttpSessionListener listener =
(HttpSessionListener) listeners[j];
try {
context.fireContainerEvent(“beforeSessionDestroyed”,
listener);
listener.sessionDestroyed(event);
context.fireContainerEvent(“afterSessionDestroyed”,
listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
context.fireContainerEvent(
“afterSessionDestroyed”, listener);
} catch (Exception e) {
// Ignore
}
manager.getContext().getLogger().error
(sm.getString(“standardSession.sessionEvent”), t);
}
}
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}

if (ACTIVITY_CHECK) {
accessCount.set(0);
}

// Remove this session from our manager’s active sessions
// 从ManagerBase 中删除
manager.remove(this, true);

给大家的福利

零基础入门

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:

在这里插入图片描述

因篇幅有限,仅展示部分资料

网络安全面试题

绿盟护网行动

还有大家最喜欢的黑客技术

网络安全源码合集+工具包

所有资料共282G,朋友们如果有需要全套《网络安全入门+黑客进阶学习资源包》,可以扫描下方二维码领取(如遇扫码问题,可以在评论区留言领取哦)~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注网络安全)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

f3395407120bb0e1b5bf17bb6b6c743.png)

还有大家最喜欢的黑客技术

网络安全源码合集+工具包

所有资料共282G,朋友们如果有需要全套《网络安全入门+黑客进阶学习资源包》,可以扫描下方二维码领取(如遇扫码问题,可以在评论区留言领取哦)~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注网络安全)
[外链图片转存中…(img-XfYxMkdh-1713410746957)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值