- 需求
- 困难
- 分析
- 解决
- 根据sessionid获取session
- 遇到的问题
- 将session存储到MongoDB中
- 根据sessionid获取session
需求
- 系统管理员可以根据用户登录时的sessionid使用户的session变为无效状态,以达到强制其下线的目的。
- 要在集群环境中仍然有效,即用户在一个服务节点下线后,在其他节点也同样下线。
- 在集群环境session要共享且同步。
困难
- Servlet API中没有提供根据sessionid获取相应session的方法。
- Jetty默认不支持集群环境下的session共享
分析
- Servlet API中能够根据客户端传来的sessionid获取当前用户的session,在Servlet应用中也只能获取到当前用户的session。既然能够获取当前用户的session,那必然有根据sessionid获取session的方法,只不过出于安全的考虑没有暴露给Servlet应用。那这个根据sessionid获取session的方法肯定就在Servlet容器中。
- 在Jetty中提供了将session存储到数据库中,以达到在集群中共享的目的。
解决
根据sessionid获取session
对于Jetty,这个方法在org.eclipse.jetty.server.session.AbstractSessionManager
中。
使用Jetty容器时,Servlet中的HttpServletRequest
实例其实就是org.eclipse.jetty.server.Request
实例
public class Request implements HttpServletRequest
在org.eclipse.jetty.server.Request
中有一个获取SessionManager的方法
public SessionManager getSessionManager() {
return _sessionManager;
}
获取到的SessionManager根据使用的session方案不同,最终的实例类型会不一样,但最终的实例都有一个共同的基类org.eclipse.jetty.server.session.AbstractSessionManager
,该类中定义根据sessionid获取session的方法
public abstract AbstractSession getSession(String idInCluster);
以选用的Mongo存储方案为例,其实例为org.eclipse.jetty.nosql.mongodb.MongoSessionManager
在org.eclipse.jetty.nosql.NoSqlSessionManager
中的实现为
public AbstractSession getSession(String idInCluster)
{
NoSqlSession session = _sessions.get(idInCluster);
__log.debug("getSession {} ", session );
if (session==null)
{
//session not in this node‘s memory, load it
session=loadSession(idInCluster);
if (session!=null)
{
//session exists, check another request thread hasn‘t loaded it too
NoSqlSession race=_sessions.putIfAbsent(idInCluster,session);
if (race!=null)
{
session.willPassivate();
session.clearAttributes();
session=race;
}
else
__log.debug("session loaded ", idInCluster);
//check if the session we just loaded has actually expired, maybe while we weren‘t running
if (getMaxInactiveInterval() > 0 && session.getAccessed() > 0 && ((getMaxInactiveInterval()*1000L)+session.getAccessed()) < System.currentTimeMillis())
{
__log.debug("session expired ", idInCluster);
expire(idInCluster);
session = null;
}
}
else
__log.debug("session does not exist {}", idInCluster);
}
return session;
}
如果要获取的session不在内存中,会尝试从数据库(Mongodb)中载入
//session not in this node‘s memory, load it
session=loadSession(idInCluster);
org.eclipse.jetty.nosql.NoSqlSessionManager#loadSession
方法在org.eclipse.jetty.nosql.mongodb.MongoSessionManager
中实现
protected synchronized NoSqlSession loadSession(String clusterId) {
DBObject o = _dbSessions.findOne(new BasicDBObject(__ID, clusterId));
__log.debug("MongoSessionManager:id={} loaded={}", clusterId, o);
if (o == null)
return null;
Boolean valid = (Boolean) o.get(__VALID);
__log.debug("MongoSessionManager:id={} valid={}", clusterId, valid);
if (valid == null || !valid)
return null;
try {
Object version = o.get(getContextAttributeKey(__VERSION));
Long created = (Long) o.get(__CREATED);
Long accessed = (Long) o.get(__ACCESSED);
NoSqlSession session = null;
// get the session for the context
DBObject attrs = (DBObject) getNestedValue(o, getContextKey());
__log.debug("MongoSessionManager:attrs {}", attrs);
if (attrs != null) {
__log.debug("MongoSessionManager: session {} present for context {}", clusterId, getContextKey());
//only load a session if it exists for this context
session = new NoSqlSession(this, created, accessed, clusterId, version);
for (String name : attrs.keySet()) {
//skip special metadata attribute which is not one of the actual session attributes
if (__METADATA.equals(name))
continue;
String attr = decodeName(name);
Object value = decodeValue(attrs.get(name));
session.doPutOrRemove(attr, value);
session.bindValue(attr, value);
}
session.didActivate();
} else
__log.debug("MongoSessionManager: session {} not present for context {}", clusterId, getContextKey());
return session;
} catch (Exception e) {
LOG.warn(e);
}
return null;
}
遇到的问题
在Servlet应用中访问org.eclipse.jetty.server.Request
时,抛出以下异常
java.lang.ClassNotFoundException: org.eclipse.jetty.server.Request
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at org.eclipse.jetty.webapp.WebAppClassLoader.findClass(WebAppClassLoader.java:510)
at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:441)
at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:403)
这是Jetty的类装载机制造成的。
在Jetty中,WebAppContext中的类即WEB-INF/classes/
和WEB-INF/lib/
下的类是由org.eclipse.jetty.webapp.WebAppClassLoader
装载的,在该ClassLoader的loadClass方法中有如下代码
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
Class<?> c= findLoadedClass(name);
ClassNotFoundException ex= null;
boolean tried_parent= false;
boolean system_class=_context.isSystemClass(name);
boolean server_class=_context.isServerClass(name);
if (system_class && server_class)
{
return null;
}
if (c == null && _parent!=null && (_context.isParentLoaderPriority() || system_class) && !server_class)
{
tried_parent= true;
try
{
c= _parent.loadClass(name);
if (LOG.isDebugEnabled())
LOG.debug("loaded " + c);
}
catch (ClassNotFoundException e)
{
ex= e;
}
}
if (c == null)
{
try
{
c= this.findClass(name);
}
catch (ClassNotFoundException e)
{
ex= e;
}
}
if (c == null && _parent!=null && !tried_parent && !server_class )
c= _parent.loadClass(name);
if (c == null && ex!=null)
throw ex;
if (resolve)
resolveClass(c);
if (LOG.isDebugEnabled())
LOG.debug("loaded {} from {}",c,c==null?null:c.getClassLoader());
return c;
}
从以上代码中可以看出,WebAppClassLoader
是拒绝装载server_class
的,既然如此,那只需要将org.eclipse.jetty.server.Request
排除在server_class
之外就可以在应用中使用了,可以在WEB-INF/jetty-env.xml中进行配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<Set name="serverClasses">
<Array type="java.lang.String">
<Item>-org.eclipse.jetty.jmx.</Item>
<Item>-org.eclipse.jetty.util.annotation.</Item>
<Item>-org.eclipse.jetty.continuation.</Item>
<Item>-org.eclipse.jetty.jndi.</Item>
<Item>-org.eclipse.jetty.jaas.</Item>
<Item>-org.eclipse.jetty.servlets.</Item>
<Item>-org.eclipse.jetty.servlet.DefaultServlet</Item>
<Item>-org.eclipse.jetty.jsp.</Item>
<Item>-org.eclipse.jetty.servlet.listener.</Item>
<Item>-org.eclipse.jetty.websocket.</Item>
<Item>-org.eclipse.jetty.apache.</Item>
<Item>-org.eclipse.jetty.util.log.</Item>
<Item>-org.eclipse.jetty.servlet.ServletContextHandler.Decorator</Item>
<Item>org.objectweb.asm.</Item>
<Item>org.eclipse.jdt.</Item>
<Item>-org.eclipse.jetty."</Item>
</Array>
</Set>
</Configure>
注意:虽然Jetty在启动时也会加载WEB-INF/jetty-web.xml,但以上配置写在WEB-INF/jetty-web.xml中是无效的,原因是在org.eclipse.jetty.webapp.JettyWebXmlConfiguration#configure
方法中有这样一段代码,将你的配置覆盖掉
finally
{
if (old_server_classes != null)
context.setServerClasses(old_server_classes);
}
说明:
system_class
:字面,与系统相关的类,默认包含如下包下的类,但可通过WEB-INF/jetty-env.xml配置public final static String[] __dftSystemClasses = { "java.", // Java SE classes (per servlet spec v2.5 / SRV.9.7.2) "javax.", // Java SE classes (per servlet spec v2.5 / SRV.9.7.2) "org.xml.", // needed by javax.xml "org.w3c.", // needed by javax.xml "org.eclipse.jetty.jmx.", // webapp cannot change jmx classes "org.eclipse.jetty.util.annotation.", // webapp cannot change jmx annotations "org.eclipse.jetty.continuation.", // webapp cannot change continuation classes "org.eclipse.jetty.jndi.", // webapp cannot change naming classes "org.eclipse.jetty.jaas.", // webapp cannot change jaas classes "org.eclipse.jetty.websocket.", // webapp cannot change / replace websocket classes "org.eclipse.jetty.util.log.", // webapp should use server log "org.eclipse.jetty.servlet.ServletContextHandler.Decorator", // for CDI / weld use "org.eclipse.jetty.servlet.DefaultServlet", // webapp cannot change default servlets "org.eclipse.jetty.jsp.JettyJspServlet", //webapp cannot change jetty jsp servlet "org.eclipse.jetty.servlets.AsyncGzipFilter" // special case for AsyncGzipFilter } ;
server_class
: 字面,Jetty自身的类,默认包含 如下包下的类,但可通过WEB-INF/jetty-env.xml配置public final static String[] __dftServerClasses = { "-org.eclipse.jetty.jmx.", // don‘t hide jmx classes "-org.eclipse.jetty.util.annotation.", // don‘t hide jmx annotation "-org.eclipse.jetty.continuation.", // don‘t hide continuation classes "-org.eclipse.jetty.jndi.", // don‘t hide naming classes "-org.eclipse.jetty.jaas.", // don‘t hide jaas classes "-org.eclipse.jetty.servlets.", // don‘t hide jetty servlets "-org.eclipse.jetty.servlet.DefaultServlet", // don‘t hide default servlet "-org.eclipse.jetty.jsp.", //don‘t hide jsp servlet "-org.eclipse.jetty.servlet.listener.", // don‘t hide useful listeners "-org.eclipse.jetty.websocket.", // don‘t hide websocket classes from webapps (allow webapp to use >ones from system classloader) "-org.eclipse.jetty.apache.", // don‘t hide jetty apache impls "-org.eclipse.jetty.util.log.", // don‘t hide server log "-org.eclipse.jetty.servlet.ServletContextHandler.Decorator", // don‘t hide CDI / weld interface "org.objectweb.asm.", // hide asm used by jetty "org.eclipse.jdt.", // hide jdt used by jetty "org.eclipse.jetty." // hide other jetty classes } ;
_parentLoaderPriority
: 当为true时,会先尝试使用WebAppClassLoader
的父装载器装载类,如果不成功再使用WebAppClassLoader
从WEB-INF/classes/
和WEB-INF/lib/
中进行装载;当为false时,则优先使用WebAppClassLoader
;该参数不影响本文讨论的问题,只影响类的搜寻路径的优先级,如在容器中有一个A1.0.jar
的包,在WEB-INF/lib/
是有一个A1.2.jar
的包,如果_parentLoaderPriority==true
会加载容器中的A1.0.jar
,否则会加载WEB-INF/lib/A1.2.jar
将session存储到MongoDB中
先阅读这里的官方文档
原理:
- 创建的session会存储到MongoDB中
- 如果客户端传来的sessionid,则在创建之前,会先根据sessionid到MongoDB中查看是否已经存在
- 访问session(调用getAttribute)时,会到MongoDB中刷新session
注意:
- 通过sessionid获取到的session,无法通过setAttribute方法更改其属性值
org.eclipse.jetty.nosql.mongodb.MongoSessionManager
中的一些关键代码
- 刷新session
protected Object refresh(NoSqlSession session, Object version) {
__log.debug("MongoSessionManager:refresh session {}", session.getId());
// check if our in memory version is the same as what is on the disk
if (version != null) {
DBObject o = _dbSessions.findOne(new BasicDBObject(__ID, session.getClusterId()), _version_1);
if (o != null) {
Object saved = getNestedValue(o, getContextAttributeKey(__VERSION));
if (saved != null && saved.equals(version)) {
__log.debug("MongoSessionManager:refresh not needed session {}", session.getId());
return version;
}
version = saved;
}
}
// If we are here, we have to load the object
DBObject o = _dbSessions.findOne(new BasicDBObject(__ID, session.getClusterId()));
// If it doesn‘t exist, invalidate
if (o == null) {
__log.debug("MongoSessionManager:refresh:marking session {} invalid, no object", session.getClusterId());
session.invalidate();
return null;
}
// If it has been flagged invalid, invalidate
Boolean valid = (Boolean) o.get(__VALID);
if (valid == null || !valid) {
__log.debug("MongoSessionManager:refresh:marking session {} invalid, valid flag {}", session.getClusterId(), valid);
session.invalidate();
return null;
}
// We need to update the attributes. We will model this as a passivate,
// followed by bindings and then activation.
session.willPassivate();
try {
DBObject attrs = (DBObject) getNestedValue(o, getContextKey());
//if disk version now has no attributes, get rid of them
if (attrs == null || attrs.keySet().size() == 0) {
session.clearAttributes();
} else {
//iterate over the names of the attributes on the disk version, updating the value
for (String name : attrs.keySet()) {
//skip special metadata field which is not one of the session attributes
if (__METADATA.equals(name))
continue;
String attr = decodeName(name);
Object value = decodeValue(attrs.get(name));
//session does not already contain this attribute, so bind it
if (session.getAttribute(attr) == null) {
session.doPutOrRemove(attr, value);
session.bindValue(attr, value);
} else //session already contains this attribute, update its value
{
session.doPutOrRemove(attr, value);
}
}
// cleanup, remove values from session, that don‘t exist in data anymore:
for (String str : session.getNames()) {
if (!attrs.keySet().contains(encodeName(str))) {
session.doPutOrRemove(str, null);
session.unbindValue(str, session.getAttribute(str));
}
}
}
/*
* We are refreshing so we should update the last accessed time.
*/
BasicDBObject key = new BasicDBObject(__ID, session.getClusterId());
BasicDBObject sets = new BasicDBObject();
// Form updates
BasicDBObject update = new BasicDBObject();
sets.put(__ACCESSED, System.currentTimeMillis());
// Do the upsert
if (!sets.isEmpty()) {
update.put("$set", sets);
}
_dbSessions.update(key, update, false, false, WriteConcern.SAFE);
session.didActivate();
return version;
} catch (Exception e) {
LOG.warn(e);
}
return null;
}
调用关系
- 载入session
protected synchronized NoSqlSession loadSession(String clusterId) {
DBObject o = _dbSessions.findOne(new BasicDBObject(__ID, clusterId));
__log.debug("MongoSessionManager:id={} loaded={}", clusterId, o);
if (o == null)
return null;
Boolean valid = (Boolean) o.get(__VALID);
__log.debug("MongoSessionManager:id={} valid={}", clusterId, valid);
if (valid == null || !valid)
return null;
try {
Object version = o.get(getContextAttributeKey(__VERSION));
Long created = (Long) o.get(__CREATED);
Long accessed = (Long) o.get(__ACCESSED);
NoSqlSession session = null;
// get the session for the context
DBObject attrs = (DBObject) getNestedValue(o, getContextKey());
__log.debug("MongoSessionManager:attrs {}", attrs);
if (attrs != null) {
__log.debug("MongoSessionManager: session {} present for context {}", clusterId, getContextKey());
//only load a session if it exists for this context
session = new NoSqlSession(this, created, accessed, clusterId, version);
for (String name : attrs.keySet()) {
//skip special metadata attribute which is not one of the actual session attributes
if (__METADATA.equals(name))
continue;
String attr = decodeName(name);
Object value = decodeValue(attrs.get(name));
session.doPutOrRemove(attr, value);
session.bindValue(attr, value);
}
session.didActivate();
} else
__log.debug("MongoSessionManager: session {} not present for context {}", clusterId, getContextKey());
return session;
} catch (Exception e) {
LOG.warn(e);
}
return null;
}
调用关系