HTTP is a stateless communication protocol. Each request is independent of the other, and the server cannot recognize a request that has come before. For Web applications, the activity is dependent on a state, such as user login, and HTTP requires the ability to provide login information for subsequent requests after one login request. This article was first published in the public number Epiphany source.
The solution is to use cookies, which are returned by the server to the browser, which caches and submits the Cookie data to the server on each request. Cookies are transmitted in clear text in the request and have a size limit of 4KB. Obviously, it is not feasible to save all state data in the browser. The mainstream approach is:
- When the browser makes the first request, the server assigns a unique identifier to the user, returns it and stores it in the browser’s Cookies
- The server maintains a global request status library internally and associates each request status information with a generated unique identifier
- Subsequent requests from the browser submit unique identifiers to the server to retrieve status information from previous requests
For ease of management, the server calls the entire process a Session and abstracts it into a Session class that identifies and stores information or state about the user.
Next, the source code implementation of Tomcat will be analyzed through the analysis and generation of Session identifiers, Session creation, destruction and persistence, etc. The version is 6.0.53.
1. Parse session identifiers
As the most commonly used session tracking mechanism, cookies are supported by all Servlet containers, and Tomcat is no exception. In Tomcat, the standard name for cookies that store session identifiers is JSESSIONID.
If the browser does not support cookies, you can also use the following method to record the identifier:
- URL rewriting: include the URL as a path parameter, such as /path; JSESSIONID=xxx
- URL request parameters: Add session unique identifiers as query parameters to all links on the page, such as /path? JSESSIONID=xxx
- FORM Hidden Field: A hidden field is used in the FORM to store unique values, which are submitted to the server with the FORM
Tomcat implements extracting jsessionids from URL rewrite paths and cookies. Before analyzing the source code, let’s first look at the key information about the header fields of the response and request with Cookie:
/ / set the Cookie
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=56AE5B92C272EA4F5E0FBFEFE6936C91; Path=/examples
Date: Sun, 12 May 2019 01:40:35 GMT
/ / submit a Cookie
GET /examples/servlets/servlet/SessionExample HTTP/1.1
Host: localhost:8080
Cookie: JSESSIONID=56AE5B92C272EA4F5E0FBFEFE6936C91
Copy the code
1.1 Rewrite paths from urls
A URL containing the session ID path parameter looks like this:
http://localhost:8080/examples/SessionExample; JSESSIONID=1234; n=v/? x=xCopy the code
Simplified, is looking for matching the semicolon and finally a diagonal lines between the JSESSIONID, that is the case, only the Tomcat operation is a byte, the core code in CoyoteAdapter. ParsePathParameters () method, which is not posted here.
1.2 From the Cookie header field
The method call that triggers Cookie resolution is as follows:
CoyoteAdapter. Service (Request, Response) └ ─ CoyoteAdapter. PostParseRequest (Request, Request, Response, The Response) └ ─ CoyoteAdapter. ParseSessionCookiesId (Request, Request) └ ─ Cookies. GetCookieCount () └ ─ Cookies, processCookies (MimeHeaders) └ ─ Cookies, processCookieHeader (byte[].int.int)
Copy the code
The processCookieHeader operates on bytes, and parsing may seem unintuitive. There is also a deprecated string parsing method inside Tomcat that helps you understand:
private void processCookieHeader( String cookieString ){
// Multiple cookie values are separated by commas
StringTokenizer tok = new StringTokenizer(cookieString, ";".false);
while (tok.hasMoreTokens()) {
String token = tok.nextToken();
// Get the position of the equal sign
int i = token.indexOf("=");
if (i > -1) {
// Get name and value and remove Spaces
String name = token.substring(0, i).trim();
String value = token.substring(i+1, token.length()).trim();
// RFC 2109 and bug"
value=stripQuote( value );
// Get a ServerCookie object from the internal cookie cache pool
ServerCookie cookie = addCookie();
// Set name and value
cookie.getName().setString(name);
cookie.getValue().setString(value);
} else {
// we have a bad cookie.... just let it go}}}Copy the code
Once parsed, the next step is to traverse the parseSessionCookiesId method and try to match the Cookie named JSESSIONID, if present, to requestedSessionId of Request. Associated with an internal Session object.
2. Generate session cookies
Session related cookies are generated internally by Tomcat. When the Servlet uses Request.getSession() to obtain the session object, the execution will be triggered. Core code:
protected Session doGetSession(boolean create) {...// Create Session instance
if (connector.getEmptySessionPath() && isRequestedSessionIdFromCookie()) {
// If the session ID comes from a cookie, reuse the ID; if it comes from a URL, do not
// Reuse the session ID to prevent possible phishing attacks
session = manager.createSession(getRequestedSessionId());
} else {
session = manager.createSession(null);
}
// Create a new Session cookie based on the Session
if((session ! =null) && (getContext() ! =null)
&& getContext().getCookies()) {
String scName = context.getSessionCookieName();
if (scName == null) {
/ / the default JSESSIONID
scName = Globals.SESSION_COOKIE_NAME;
}
/ / new cookies
Cookie cookie = new Cookie(scName, session.getIdInternal());
// Set path Domain Secure
configureSessionCookie(cookie);
// Add to the response header field
response.addSessionCookieInternal(cookie, context.getUseHttpOnly());
}
if(session ! =null) {
session.access();
return (session);
} else {
return (null); }}Copy the code
To add to the response header field, the Cookie object is generated in the format described at the beginning.
3. Session
Session is an internal Tomcat interface, an HttpSession facade class, used to maintain state information between requests from specific users of web applications. Related class diagram design is as follows:
Key classes or interfaces do the following:
- Manager – Manages a pool of sessions, with different implementations providing specific capabilities such as persistence and distribution
- ManagerBase – Implements basic features such as Session pooling, unique ID generation algorithms, and easy inheritance extensions
- StandardManager – Standard implementation that provides simple session persistence when this component is restarted (for example, when an entire server is shut down and restarted, or when a specific Web application is reloaded)
- PersistentManagerBase – Provides many different ways to manage persistent storage, such as files and databases
- Store – Provides persistent storage and loading of session and user information
- ClusterManager – Cluster Session management interface responsible for the replication mode of sessions
- DeltaManager – Replicates session data increments to all members of the cluster
- BackupManager – Copies data to only one backup node that is visible to all members of the cluster
This article does not analyze the mechanism of cluster replication, but only the management of single Session.
3.1 create a Session
When request.getSession () is first used in the Servlet to retrieve the session object, a StandardSession instance is created:
public Session createSession(String sessionId) {
// New StandardSession(this) is returned by default
Session session = createEmptySession();
// Initialize the property
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
// Set the session validity period, in seconds. The default value is 30 minutes. A negative value indicates that the session never expires
session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
if (sessionId == null) {
// Generate a session ID
sessionId = generateSessionId();
session.setId(sessionId);
sessionCounter++;
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return (session);
}
Copy the code
The key lies in the generation of unique session identifiers. Let’s take a look at Tomcat’s generation algorithm:
- Get 16 bytes randomly
- Using MD5 to encrypt these bytes, you get a 16-byte array again
- Iterate over the new byte array, generating a hexadecimal character using the high and low four bits of each byte
- You end up with a 32-bit hexadecimal string
The core code is as follows:
protected String generateSessionId(a) {
byte random[] = new byte[16];
String jvmRoute = getJvmRoute();
String result = null;
// Render the result as a string of hexadecimal numbers
StringBuffer buffer = new StringBuffer();
do {
int resultLenBytes = 0;
if(result ! =null) { // repeat, regenerate
buffer = new StringBuffer();
duplicates++;
}
/ / sessionIdLength is 16
while (resultLenBytes < this.sessionIdLength) {
getRandomBytes(random);// Randomly get 16 bytes
// Get the 16-byte digest. MD5 is used by default
random = getDigest().digest(random);
// Iterate over the byte array to generate a 32-bit hexadecimal string
for (int j = 0;
j < random.length && resultLenBytes < this.sessionIdLength;
j++) {
// Generates a hexadecimal character using the high and low four bits of the specified byte
byte b1 = (byte) ((random[j] & 0xf0) > >4);
byte b2 = (byte) (random[j] & 0x0f);
// Convert to hexadecimal numeric characters
if (b1 < 10) {buffer.append((char) ('0'+ b1)); }// Convert to uppercase hexadecimal characters
else {buffer.append((char) ('A' + (b1 - 10))); }if (b2 < 10) {buffer.append((char) ('0'+ b2)); }else {buffer.append((char) ('A' + (b2 - 10)));}
resultLenBytes++;
}
}
if(jvmRoute ! =null) {buffer.append('. ').append(jvmRoute); } result = buffer.toString(); }while (sessions.containsKey(result));
return (result);
}
Copy the code
3.2 Session Expiration Check
One Web application corresponds to one session Manager, which means StandardContext has an instance of Manager within it. Each container component starts a background thread that periodically calls itself and the internal component’s backgroundProcess() method. The Manager daemon checks whether the Session has expired.
The logic is to get all sessions using isValid to determine whether they are expired or not, as shown in the following code:
public boolean isValid(a) {...// Whether to check for activity. Default is false
if (ACTIVITY_CHECK && accessCount.get() > 0) {
return true;
}
// Check whether the time has expired
if (maxInactiveInterval >= 0) {
long timeNow = System.currentTimeMillis();
int timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L);
if (timeIdle >= maxInactiveInterval) {
// If it expires, do some internal processing
// 主要是通知对过期事件感兴趣的 listeners
expire(true); }}// Plural never expires
return (this.isValid);
}
Copy the code
3.3 Session Persistence
Persistence is the serialization of an active Session object in memory to a file or to a database. If the session manager component complies with and enables persistence, then the storage is executed in its lifecycle event stop method; Load is performed in the start method.
Persist to file, StandardManager also provides the ability to persist to file, It writes all active SESSIONS from the session pool to CATALINA_HOME/work/Catalina/
/< webApp >/SESSIONS. Ser in its doUnload method.
FileStore also provides persistence to files, which differs from StandardManager in that it writes each session to a single file named < ID >.session.
Persist to database, store session-related data respectively in a table, including binary data after serialization, table field information is as follows:
create table tomcat_sessions (
session_id varchar(100) not null primary key,
valid_session char(1) not null.-- Whether it is effective
max_inactive int not null.-- Maximum valid time
last_access bigint not null.-- Last access time
app_name varchar(255), -- Application name in the format of /Engine/Host/Context
session_data mediumblob, -- Binary data
KEY kapp_name(app_name)
);
Copy the code
Note: You need to place the jar file for the database driver in the $CATALINA_HOME/lib directory to make it visible to the class loader inside Tomcat.
4. Summary
This article briefly analyzes the Tomcat Session management, of course, ignored a lot of details, interested in the source code can be in-depth, the follow-up will be the implementation of Tomcat cluster Session analysis. If you have any questions, please leave a message.
Search the wechat public account “Epiphany source code” to get more source code analysis and build the wheel.