由于HTTP是一种无状态协议,当用户的一次访问请求结束后,后端服务器就无法知道下一次来访问的还是不是
上次访问的用户;提到Session与Cookie,二者的作用都是为了保持访问用户与后端服务器的交互状态,解决了HTTP
是一种无状态协议的弊端。
- Cookie
在Tomcat 8中是如何调用addCookie方法将Cookie加到HTTP的Header中的???
根据上图可知真正的Cookie构建是在org.apache.catalina.connector.Response中完成的:
/**
* Add the specified Cookie to those that will be included with
* this Response.
*
* @param cookie Cookie to be added
*/
@Override
public void addCookie(final Cookie cookie) {
// Ignore any call from an included servlet
if (included || isCommitted()) {
return;
}
cookies.add(cookie);
// 此时 若未设置版本号Version 0 或 Version 1,Tomcat在最后构建HTTP响应头时也会自动将其设置为Version 1
String header = generateCookieString(cookie);
//if we reached here, no exception, cookie is valid
// the header name is Set-Cookie for both "old" and v.1 ( RFC2109 )
// RFC2965 is not supported by browsers and the Servlet spec
// asks for 2109.
addHeader("Set-Cookie", header, getContext().getCookieProcessor().getCharset());
}
注意:所创建Cookie的NAME值不能和Set-Cookie或者Set-Cookie2属性项值相同!!!
Tomcat中根据response.addCookie创建多个Cookie时,这些Cookie都是创建一个以NAME为Set-Cookie的
MimeHeaders,最终在请求返回时HTTP响应头将相同Header标识的Set-Cookie值进行合并,浏览器在接收HTTP返回的
数据时将分别解析每一个Header项;合并过程在org.apache.coyote.http11.Http11Processor的prepareResponse方法中进行:
int size = headers.size();
for (int i = 0; i < size; i++) {
outputBuffer.sendHeader(headers.getName(i), headers.getValue(i));
}
- Session
当Cookie很多时,在传输的过程中,无形增加了客户端与服务端的数据传输量 以及 直接传输Cookie所带来的数据不
安全性,而Session解决了该问题;同一个客户端每次和服务端交互时,不需要每次都传回所有的Cookie值,而只要传回
一个ID,该ID由第一次访问服务端时生成,每个客户端唯一;这个ID通常是NAME为JSESSIONID的一个Cookie:就如同
宝箱 与 钥匙。按通常的了解Session的工作方式是基于Cookie的,实际上Tomcat中Session工作方式分为三种:
(一)基于URL Path Parameter,默认支持:
当浏览器不支持Cookie功能时,浏览器会将用户的SessionCookieName重写到用户请求的URL参数中,格式为:
/cxt/login.do;SessionCookieName=3EF0AEC40F2C6C30A0580844C0E6B2E8?count=8。
关于SessionCookieName,可在web.xml配置:
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<name>SessionCookieName</name>
</cookie-config>
</session-config>
否则默认为" JSESSIONID "。然后Request会根据这个SessionCookieName到Paramters中获取到SessionID并设置
到org.apache.catalina.connector.Request中的requestedSessionId属性中。
(二)基于Cookie,如果没有修改Context容器中的Cookies标识,默认也是支持的。
如果客户端也支持Cookie,则Tomcat中仍会解析Cookie中的SessionID并会覆盖URL中的SessionID.
(三)基于SSL,默认不支持,只有在connector.getAttribute("SSLEnabled")为true时才支持
Session在Tomcat中如何工作:
HttpSession对象,实际上是StandardSession对象的门面类对象;通过 HttpServletRequest request.getSession
( boolean create ) 创建,并将创建后的HttpSession对象添加到org.apache.catalina.Manager的sessions容器中保存,
只要HttpSession对象存在,便可根据SessionID来获取该对象,一个requestedSessionId对应一个访问的客户端,一
个客户端对应一个 StandardSession,时序图如下:
StandardManager类如何管理StandardSession???
Manager实现类是org.apache.catalina.session.StandardManager;StandardManager类将管理所有的Session
对象的生命周期,过期被收回,服务器关闭,被序列化到磁盘等。
当Servlet容器重启或关闭时,StandardManager负责持久化没有过期的StandardSession对象,调用doUnload方法
将其持久化到一个以" SESSIONS.ser "为文件名的文件中;当Servlet容器重启时,即StandardManager初始化时,调用
doLoad方法会重新读取该文件,解析所有Session对象,重新保存在StandardManager的sessions集合中:
/**
* Path name of the disk file in which active sessions are saved
* when we stop, and from which these sessions are loaded when we start.
* A <code>null</code> value indicates that no persistence is desired.
* If this pathname is relative, it will be resolved against the
* temporary working directory provided by our context, available via
* the <code>javax.servlet.context.tempdir</code> context attribute.
*/
protected String pathname = "SESSIONS.ser";
/**
* 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.
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");
}
}
/**
* 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<StandardSession> 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()));
Iterator<Session> elements = sessions.values().iterator();
while (elements.hasNext()) {
StandardSession session =
(StandardSession) elements.next();
list.add(session);
session.passivate();
session.writeObjectData(oos);
}
}
}
// Expire all the sessions we just wrote
if (log.isDebugEnabled()) {
log.debug("Expiring " + list.size() + " persisted sessions");
}
Iterator<StandardSession> expires = list.iterator();
while (expires.hasNext()) {
StandardSession session = expires.next();
try {
session.expire(false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
session.recycle();
}
}
if (log.isDebugEnabled()) {
log.debug("Unloading complete");
}
}
/**
* Return a File object representing the pathname to our
* persistence file, if any.
* @return the file
*/
protected File file() {
if (pathname == null || pathname.length() == 0) {
return null;
}
File file = new File(pathname);
if (!file.isAbsolute()) {
Context context = getContext();
ServletContext servletContext = context.getServletContext();
File tempdir = (File) servletContext.getAttribute(ServletContext.TEMPDIR);
if (tempdir != null) {
file = new File(tempdir, pathname);
}
}
return file;
}
StandardSession对象并不是永远存在的,防止内存空间消耗殆尽,会为每个Session对象设置有效时间???
Tomcat中默认有效时间是30分钟,由(maxInactiveInterval属性控制),超过30分钟会过期,在后台线
程backgroundProcess方法中检查每个StandardSession是否过期;
request.getSession( boolean create )该方法调用时也会检查Session对象是否过期,如果过期,会重新创
建一个StandardSession,但是以前设置的Session值会全部消失,所以如果session.getAttribute( )获取不到以前
的值请勿大惊小怪。
注意:
request.getSession(true):若存在会话则返回该会话,否则新建一个会话。
request.getSession(false):若存在会话则返回该会话,否则返回NULL。
如何自定义封装HttpSession对象???
在应用的web.xml中配置一个SessionFilter,用于在请求到达MVC框架之前封装HttpServletRequest和
HttpServletResponse对象,并创建自定义InnerHttpSession,并将其设置到request及response对象中。此时通过
request.getSession(boolean create)获取的便是自定义的HttpSession对象,并将个别重要的Session通过拦截
response的addCookies保留到Cookie中留作备份。时序图如下: