HTTP 协议是一种状态的协议,WEB 服务器本身不能识别出哪些请求是同一个浏览器发出的,浏览器的每一个请求都是完全独立的。
即使 HTTP 1.1 支持持续连接,但当用户有一段时间没有提交请求,连接也会关闭。
怎么才能实现网上商店中的购物车呢:某个用户从网站的登陆页面登入后,再进入购物页面购物时,负责处理购物请求的服务器程序必须知道处理上一次请求程序所得到的用户信息。
作为 Web 服务器,必须能够采用一种机制来唯一地标识一个标识,同时记录该用户的状态。
Web 应用中的会话是指一个客户端浏览器与 Web 服务器之间连接发生的一系统请求和响应过程。
Web 应用的会话状态是指 Web 服务器与浏览器在会话过程中产生的状态信息,借助会话状态, Web 服务器能够把属于同一会话中的一系列请求和响应过程关联起来
Web 服务器端程序要能从大量的请求消息中区分出哪些请求消息属于同一个会话,即能识别出来逢同一个浏览器的访问请求,这需要浏览器对其发出的每个请求消息都进行标识:属于同一个会话中的请求消息都附带同样的标识号,而属于不同会话的请求消息总是附带不同的标识号,这个标识号就称之为会话 ID(SessionId)
在 Servlet 规范中,常用以下两种机制完成会话跟踪
cookie 机制采用的是在**客户端保存 HTTP 状态信息的方案
Cookie 是在浏览器访问 Web 服务器的某个资源时,由 Web 服务器在 Http 响应头中附带传送给浏览器的一个小文本文件。
一旦 Web 浏览器保存了某个 cookie,那么它在以后每次访问该 Web 服务器时,都会在 HTTP 请求头中将这个 Cookie 回传给 Web 服务器。
底层的实现原理:Web 服务器通过在 HTTP 响应消息中增加 Set-Cookie 响应头 字段将 Cookie 信息发送给浏览器,浏览器则通过在 HTTP 请求信息中增加 Cookie 请求头字段将 Cookie 回传给 Web 服务器。
HttpServletRequest 接口提供了getCookies方法来获取请求中出现的 cookies 数组。这些 cookie 是客户端在每次请求时从客户端发送到服务器的数据。通常,客户机作为 cookie 的一部分返回的唯一信息是 cookie 名称和 cookie 值。在将 cookie 发送到浏览器时可以设置的其他 cookie 属性,比如注释,通常不会返回。该规范还允许 cookie 为HttpOnly cookie。HttpOnly cookie向客户端表明它们不应该被暴露给客户端脚本代码(除非客户端知道寻找这个属性,否则它不会被过滤掉)。使用 HttpOnly cookie有助于减轻某些类型的跨站点脚本攻击。HttpServletResponse 接口提供了addCookie 方法来添加 cookie 对象。
Cookie 总结如下
Cookie 操作工具类
public class CookieUtils {
public static String getCookie(HttpServletRequest request, String cookieName){
//Cookie的默认有效期是一次会话中。
Cookie[] cookies = request.getCookies();
String value = null;
if(cookies != null){
for (Cookie cookie : cookies) {
//遍历出每一个cookie对象,咱们怎么去判断该cookie是否是我们想要的那个呢?
//通过cookie的name进行判断
String name = cookie.getName();
if (name.equals(cookieName)) {
//获取cookie的value
value = cookie.getValue();
break;
}
}
}
return value;
}
public static void addCookie(HttpServletResponse response, String name, String value, int time, String path){
// 1.创建Cookie对象
Cookie cookie = new Cookie(name, value);
// 2.设置该cookie的最大有效期
cookie.setMaxAge(time);
// 3.设置有效范围
cookie.setPath(path);
// 4.添加 cookie
response.addCookie(cookie);
}
public static void deleteCookie(HttpServletResponse response, String name){
// 1.创建Cookie对象
Cookie cookie = new Cookie(name, null);
// 2.设置该cookie的最大有效期
cookie.setMaxAge(0);
// 3.添加到 response
response.addCookie(cookie);
}
}
session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话是从拿起电话拨号到挂断电话这中间的一系列过程可以称这为一个 Session。session 在 Web 开发环境下的语义以有了新的扩展,它的含义是指 一类用来在客户端与服务器端之间保持状态的解决方案。有时候 Session 也用来指这种解决方案的储存结构
session 机制采用的是在服务器端保持 HTTP 状态信息的方案。服务器使用散列表的结构来保存信息。当程序需要为某个客户端的请求创建一个 Session 时,服务器首先检查这个客户端的请求里是否包含了一个 Session 标识(即 sessionId),如果已经包含一个 sessionId 则说明以前已经为此客户创建过 session,服务器就按照 sessionId 把这个 session 检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的 session 对象,但用户人为地在请求的 URL 后面附加一个 JSESSION 的参数)。如果客户请求不包含 sessionId,则为此客户创建一个 session 并且生成一个与此 session 相关联的 sessionId,这个 sessionId 将在本次响应中返回给客户端保存。
HelloWorldServlet.java
public class HelloWorldServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
session.setAttribute("name", "carl");
System.out.println("invoke HelloWorldServlet.doGet success");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("invoke HelloWorldServlet.doPost success");
}
}
以chrome浏览器为例,访问一个基于tomcat服务器的网站的时候,
浏览器第一次访问服务器,服务器会在响应头添加Set-Cookie:“JSESSIONID=XXXXXXX”
信息,要求客户端设置cookie,如下图:
接下来,浏览器第二次、第三次…访问服务器,观察其请求头的cookie信息,可以看到JSESSIONID信息存储在cookie里,发送给服务器;且响应头里没有Set-Cookie信息,如下图:
只要浏览器未关闭,在访问同一个站点的时候,其请求头Cookie中的JSESSIONID都是同一个值,被服务器认为是同一个会话。
当请求过程中首先要解析请求中的 sessionId
信息,然后将 sessionId
存储到 request 的参数列表中。然后再从 Request
获取 Session
的时候,如果存在 sessionId
那么就根据 Id 从 Session
池中获取 session
,如果 sessionId
不存在或者 Session
失效,那么则新建 Session
并且将 Session
信息放入缓存池,供下次使用。
sessionid
的默认 Key 是 jsessionid
一般客户端请求服务端时间保存在 2 个地方:
jsessionId
保存在 URL 信息当中。比如:http://localhost:8080/web-demo/servlets/hello;jsessionid=123456789
。这种方式其实是当客户端禁用 Cookie
,只能通过 URL 重写的方式把 sessionId 信息在禁用 Cookie 的情况下通过重写 URL 带到服务端。CoyoteAdapter#parsePathParameters
就是 Cookie 禁用
的情况下把 jsessionid
以 url 的形式传入到服务端:CoyoteAdapter#parsePathParameters
protected void parsePathParameters(org.apache.coyote.Request req,
Request request) {
// Process in bytes (this is default format so this is normally a NO-OP
req.decodedURI().toBytes();
ByteChunk uriBC = req.decodedURI().getByteChunk();
// Cookie 禁用通过 URL 的方式传入到服务端,例如:http://localhost:8080/web-demo/servlets/hello;jsessionid=123456789
int semicolon = uriBC.indexOf(';', 0);
// Performance optimisation. Return as soon as it is known there are no
// path parameters;
if (semicolon == -1) {
return;
}
// What encoding to use? Some platforms, eg z/os, use a default
// encoding that doesn't give the expected result so be explicit
Charset charset = connector.getURICharset();
if (log.isDebugEnabled()) {
log.debug(sm.getString("coyoteAdapter.debug", "uriBC",
uriBC.toString()));
log.debug(sm.getString("coyoteAdapter.debug", "semicolon",
String.valueOf(semicolon)));
log.debug(sm.getString("coyoteAdapter.debug", "enc", charset.name()));
}
while (semicolon > -1) {
// Parse path param, and extract it from the decoded request URI
int start = uriBC.getStart();
int end = uriBC.getEnd();
int pathParamStart = semicolon + 1;
int pathParamEnd = ByteChunk.findBytes(uriBC.getBuffer(),
start + pathParamStart, end,
new byte[] {';', '/'});
String pv = null;
if (pathParamEnd >= 0) {
if (charset != null) {
pv = new String(uriBC.getBuffer(), start + pathParamStart,
pathParamEnd - pathParamStart, charset);
}
// Extract path param from decoded request URI
byte[] buf = uriBC.getBuffer();
for (int i = 0; i < end - start - pathParamEnd; i++) {
buf[start + semicolon + i]
= buf[start + i + pathParamEnd];
}
uriBC.setBytes(buf, start,
end - start - pathParamEnd + semicolon);
} else {
if (charset != null) {
pv = new String(uriBC.getBuffer(), start + pathParamStart,
(end - start) - pathParamStart, charset);
}
uriBC.setEnd(start + semicolon);
}
if (log.isDebugEnabled()) {
log.debug(sm.getString("coyoteAdapter.debug", "pathParamStart",
String.valueOf(pathParamStart)));
log.debug(sm.getString("coyoteAdapter.debug", "pathParamEnd",
String.valueOf(pathParamEnd)));
log.debug(sm.getString("coyoteAdapter.debug", "pv", pv));
}
if (pv != null) {
int equals = pv.indexOf('=');
if (equals > -1) {
String name = pv.substring(0, equals);
String value = pv.substring(equals + 1);
// 解析出来的信息添加到请求参数当中
request.addPathParameter(name, value);
if (log.isDebugEnabled()) {
log.debug(sm.getString("coyoteAdapter.debug", "equals",
String.valueOf(equals)));
log.debug(sm.getString("coyoteAdapter.debug", "name",
name));
log.debug(sm.getString("coyoteAdapter.debug", "value",
value));
}
}
}
semicolon = uriBC.indexOf(';', semicolon);
}
}
上面是把 Url 重写的信息保存到 HTTP 请求信息当中。然后会在 CoyoteAdapter#postParseRequest
信息当中从 HTTP 请求参数当中获取 SessionId 信息。
上面会从两个地方获取 Session,首先从 Http 请求参数中获取也就是我们上一步的解析重写的 HTTP 看里面是否带 SessionId ,然后从 Cookie 里面获取 SessionId 信息。
protected void parseSessionCookiesId(Request request) {
// If session tracking via cookies has been disabled for the current
// context, don't go looking for a session ID in a cookie as a cookie
// from a parent context with a session ID may be present which would
// overwrite the valid session ID encoded in the URL
Context context = request.getMappingData().context;
if (context != null && !context.getServletContext()
.getEffectiveSessionTrackingModes().contains(
SessionTrackingMode.COOKIE)) {
return;
}
// Parse session id from cookies
ServerCookies serverCookies = request.getServerCookies();
int count = serverCookies.getCookieCount();
if (count <= 0) {
return;
}
String sessionCookieName = SessionConfig.getSessionCookieName(context);
for (int i = 0; i < count; i++) {
ServerCookie scookie = serverCookies.getCookie(i);
if (scookie.getName().equals(sessionCookieName)) {
// Override anything requested in the URL
if (!request.isRequestedSessionIdFromCookie()) {
// Accept only the first session id cookie
convertMB(scookie.getValue());
request.setRequestedSessionId
(scookie.getValue().toString());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
if (log.isDebugEnabled()) {
log.debug(" Requested cookie session id is " +
request.getRequestedSessionId());
}
} else {
if (!request.isRequestedSessionIdValid()) {
// Replace the session id until one is valid
convertMB(scookie.getValue());
request.setRequestedSessionId
(scookie.getValue().toString());
}
}
}
}
}
从上面的代码可以看到如果 HTTP 的 URL 里面与 Cookie 信息里面同时存在 Session 信息那么就会以 Cookie 里面的 SessionId 为先。
下面就是服务器端获取或者生成 Session 的时序图。
默认情况下,服务器端是没有 Session 信息的,只有在业务代码当中有 HttpServletRequest#getSession 才会触发服务端生成对应请求的 Session 信息。getSession 有两个重载方法:
在 Apache Tomcat 当中通过 org.apache.catalina.Manager
来管理 HttpSession 信息。
public interface Manager {
// ------------------------------------------------------------- Properties
public Context getContext();
public void setContext(Context context);
public SessionIdGenerator getSessionIdGenerator();
public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);
public long getSessionCounter();
public void setSessionCounter(long sessionCounter);
public int getMaxActive();
public void setMaxActive(int maxActive);
public int getActiveSessions();
public long getExpiredSessions();
public void setExpiredSessions(long expiredSessions);
public int getRejectedSessions();
public int getSessionMaxAliveTime();
public void setSessionMaxAliveTime(int sessionMaxAliveTime);
public int getSessionAverageAliveTime();
public int getSessionCreateRate();
public int getSessionExpireRate();
// --------------------------------------------------------- Public Methods
public void add(Session session);
public void addPropertyChangeListener(PropertyChangeListener listener);
public void changeSessionId(Session session);
public void changeSessionId(Session session, String newId);
public Session createEmptySession();
public Session createSession(String sessionId);
public Session findSession(String id) throws IOException;
public Session[] findSessions();
public void load() throws ClassNotFoundException, IOException;
public void remove(Session session);
public void remove(Session session, boolean update);
public void removePropertyChangeListener(PropertyChangeListener listener);
public void unload() throws IOException;
public void backgroundProcess();
public boolean willAttributeDistribute(String name, Object value);
}
里面包含了 Session 的管理方法:
下面是 SessionId 的 key 规则,Cookie 的 SessionId 默认是 jsessionid
,而重写 URL 里面的 SessionId 默认是 JSESSIONID
。
public class SessionConfig {
private static final String DEFAULT_SESSION_COOKIE_NAME = "JSESSIONID";
private static final String DEFAULT_SESSION_PARAMETER_NAME = "jsessionid";
/**
* Determine the name to use for the session cookie for the provided
* context.
* @param context The context
* @return the cookie name for the context
*/
public static String getSessionCookieName(Context context) {
String result = getConfiguredSessionCookieName(context);
if (result == null) {
result = DEFAULT_SESSION_COOKIE_NAME;
}
return result;
}
/**
* Determine the name to use for the session path parameter for the provided
* context.
* @param context The context
* @return the parameter name for the session
*/
public static String getSessionUriParamName(Context context) {
String result = getConfiguredSessionCookieName(context);
if (result == null) {
result = DEFAULT_SESSION_PARAMETER_NAME;
}
return result;
}
private static String getConfiguredSessionCookieName(Context context) {
// Priority is:
// 1. Cookie name defined in context
// 2. Cookie name configured for app
// 3. Default defined by spec
if (context != null) {
String cookieName = context.getSessionCookieName();
if (cookieName != null && cookieName.length() > 0) {
return cookieName;
}
SessionCookieConfig scc =
context.getServletContext().getSessionCookieConfig();
cookieName = scc.getName();
if (cookieName != null && cookieName.length() > 0) {
return cookieName;
}
}
return null;
}
/**
* Determine the value to use for the session cookie path for the provided
* context.
*
* @param context The context
* @return the parameter name for the session
*/
public static String getSessionCookiePath(Context context) {
SessionCookieConfig scc = context.getServletContext().getSessionCookieConfig();
String contextPath = context.getSessionCookiePath();
if (contextPath == null || contextPath.length() == 0) {
contextPath = scc.getPath();
}
if (contextPath == null || contextPath.length() == 0) {
contextPath = context.getEncodedPath();
}
if (context.getSessionCookiePathUsesTrailingSlash()) {
// Handle special case of ROOT context where cookies require a path of
// '/' but the servlet spec uses an empty string
// Also ensure the cookies for a context with a path of /foo don't get
// sent for requests with a path of /foobar
if (!contextPath.endsWith("/")) {
contextPath = contextPath + "/";
}
} else {
// Only handle special case of ROOT context where cookies require a
// path of '/' but the servlet spec uses an empty string
if (contextPath.length() == 0) {
contextPath = "/";
}
}
return contextPath;
}
private SessionConfig() {
// Utility class. Hide default constructor.
}
}
默认情况下 Session 会保存在 org.apache.catalina.session.ManagerBase#sessions
这个 ConcurrentHashMap
当中。
@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 session = createEmptySession();
// Initialize the properties of the new session and return it
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);
sessionCounter++;
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return session;
}
在设置 SessionId 的时候会调用 org.apache.catalina.session.ManagerBase#add
把 Session 添加到org.apache.catalina.session.ManagerBase#sessions
当中。同样的如果从 URL 或者 Cookie 当中解析到 SessionId 就会把这个 Map 对象当中获取 Session 值。
通过上面我们可以知道 Tomcat 是通过 Manage 这个组件来管理 Session 的。下面我们来看一下 Manage 的类继承结构:
在 PersistentManagerBase 类中有个成员变量 Store,通过它使用 Manage 具有持久化的能力:
PersistentManagerBase.java
/**
* Store object which will manage the Session store.
*/
protected Store store = null;
持久化 session 管理器的存储策略就是有这个 Store 对象定义的,这个 Store 的类继承结构如下:
接口 Store 及其实例是为 session 管理器提供了一套存储策略,store定义了基本的接口,而 StoreBase
提供了基本的实现。 其中FileStore
类实现的策略是将session存储在以setDirectory()
指定目录并以.session结尾的文件中的。 JDBCStore
类是将Session
通过JDBC
存入数据库中,因此需要使用JDBCStore
,需要分别调用setDriverName()
方法和 setConnectionURL()
方法来设置驱动程序名称和连接URL。
默认的Tomcat默认会话存储机制使用临时文件。为了节省使用JDBC和MySQL的会话,遵循以下过程:
这些步骤都不涉及以任何方式修改示例会话脚本。反映Tomcat如何实现应用程序级别以上的会话支持。
Tomcat在会话表中存储了几种类型的信息:
下表满足这些规格;现在创建它,然后继续:
CREATE TABLE tomcat_session
(
id VARCHAR(32) NOT NULL,
app VARCHAR(255),
data LONGBLOB,
valid_session CHAR(1) NOT NULL,
max_inactive INT NOT NULL,
update_time BIGINT NOT NULL,
PRIMARY KEY (id),
INDEX (app)
);
将JDBC驱动程序放在Tomcat可以找到它的地方。
因为Tomcat本身管理会话,所以它必须能够访问JDBC驱动程序用于在数据库中存储会话。通常在lib目录中安装驱动程序,以便对Tomcat和应用程序都可用。(备注:如果 war 中中已经有引用mysql jdbc驱动程序则不需要专门将驱动jar包拷贝到tomcat的自由目录下)
要告诉Tomcat使用tomcat_session表,请修改mcb应用程序上下文文件。更改位置到webapps/mcb/META-INF下的 Tomcat/webapps
目录,复制context.xml。jdbc上下文。xml,并重新启动 Tomcat。
如果你看一下context.xml中,你会发现一个元素包含一个元素,该元素指定使用JDBC存储基于mysql的会话:
context.xml
编写一个获取 Session 的 HelloWorldServlet 并且存在数据到 Session 当中。
public class HelloWorldServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
session.setAttribute("name", "carl");
System.out.println("invoke HelloWorldServlet.doGet success");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("invoke HelloWorldServlet.doPost success");
}
}
在 web.xml 中添加 以上的 Servlet 。
访问 http://localhost:8080/web-demo/servlets/hello
,然后关闭 Tomcat。可以在数据库 Session 表中看到新增了一条数据:
参考文章: