一、需要会话的原因
从服务器的角度来说,当请求结束时,客户端与服务器之间就再有任何联系,如果有下一个请求开始时,就无法将新的请求与之前的请求关联起来。这是因为 HTTP请求自身是完全无状态的,会话就是用来维持请求和请求之间的状态的。
拿生活场景举例:你进入最喜欢的超市购物,找到一个购物车(从服务器获得会话),一边逛一边挑选喜爱的商品并将它们添加到购物车中(将商品添加到会话中),购物结束后将购物车中的商品取出并递给收银员,收银员扫描你购买的商品并接收你的付款(通过会话付款),付款结束后出门并还回购物车(关闭浏览器或注销,结束会话)。
二、使用会话cookie和URL重写
会话是由服务器或Web应用程序管理的某些文件、内存片段、对象或者容器,它包含了分配给它的各种不同的数据。这些数据元素可以是用户名、购物车、工作流细节等,用户浏览器中不用保持或维持任何此类数据,它们只由服务器或Web应用程序代码管理。容器和用户浏览器之间将通过某种方式连接起来,因此通常会话会被赋予一个随机生成的字符串,称为会话ID。
第一次创建会话时,服务器创建的会话ID将会作为响应的一部分返回到用户浏览器中,接下来从用户浏览器中发出的请求都将通过某种方式包含该会话ID,当应用程序收到含有会话ID的请求时,它可以通过该ID将会话与当前请求关联起来。剩下需要解决的问题就是如何将会话ID从服务器返回到浏览器中,并在之后的请求中包含该ID,目前有两种可用的技术:会话cookie和URL重写。
1、了解会话cookie
cookie是一种必要的通信机制,可以通过Set-Cookie响应头在服务器和浏览器之间传递任意的数据,并存储在用户计算机中,然后再通过请求头Cookie从浏览器返回到服务器中。cookie可以有各种不同的特性,如下:
① Domain:告诉浏览器应该将cookie发送到哪个域名中;
② Path:进一步将cookie限制在相对于域的某个特定URL中,每次浏览器发出请求时,它都将找到匹配该域和路径的所有cookie;
③ Expires:与Max-age,HTTP/1.0会优先处理该指令,它定义了cookie的绝对过期日期,如果cookie已经过期,浏览器将会立即删除它;
④ Max-age:与Expires互斥,HTTP/1.1会优先处理该指令,它定义了cookie将在多少秒后过期,如果cookie中不含有Expires和Max-age,cookie将会浏览器关闭时被删除;
⑤ Secure:(不需要有值),浏览器将只会通过HTTP发送cookie,这将保护cookie,避免以未加密的方式进行传输;
⑥ HttpOnly:它把cookie限制在直接的浏览器请求中,使得JavaScript、Flash以及其他插件无法访问cookie。
注意:尽管HttpOnly将阻止JavaScript使用doucment.cookie属性来访问cookie,但由JavaScript创建的AJAX请求仍然会包含会话ID cookie,因为是浏览器负责AJAX请求头的生成而不是JavaScript,这意味着服务器仍然能够将AJAX请求关联到用户的会话。
当Web服务器和应用服务器使用cookie在客户端存储会话ID时,这些ID将随着每次的请求被发送到服务器端,在JAVAEE应用服务器中,会话cookie的名字默认为JSESSIONID
2、URL中的会话ID
JavaEE服务器将会话ID添加到URL的最后一个路径段的矩阵参数中,通过这种方式分离开会话ID与查询字符串的参数,使它们不会互相冲突。
请求URL只会在将会话ID从浏览器发送到服务器时有效,那么第一次如何将请求URL中的会话ID从服务器发送到浏览器呢?答案是必须将会话ID内嵌在应用程序返回的所有URL中,包括页面的链接、表单操作以及302重定向。
HttpServletResponse接口定义了两个可以重写URL的方法:encodeURL和encodeRedirectURL,它们将在必要的时候将会话ID内嵌在URL中,任何在链接、表单操作或其他标签中的URL都将被传入到encodeURL方法中,然后该方法将会返回一个正确的、经过编码处理的URL(任何传入sendRedirect响应方法中的URL可以传入encodeRedirectURL方法中)。将JSESSIONID矩阵参数内嵌在URL的最后一个路径段中需要满足下面4个条件:
① 会话对于当前请求是活跃的(要么它通过传入会话ID的方式请求会话,要么应用程序创建了一个新的会话);
② JSEESSIONID cookie在请求中不存在;
③ URL不是绝对的URL,并且是同一Web应用程序中的URL;
④ 在部署描述符中已经启用了对会话URL重写的支持。
3、会话的漏洞
⑴ 复制并粘贴错误
不知情的用户决定要跟朋友分享应用程序中的某个页面,并将地址栏中的URL复制粘贴出来,那么他的朋友将看到URL中包含的会话ID,如果他们在该会话终结之前访问该URL,那么他们也会被服务器当成之前分享URL的用户,这明显会引起问题。
解决此问题的方法是:完全禁止在URL中内嵌会话ID。
⑵ 会话固定
攻击者会首先找到允许在URL中内嵌会话ID的网站,然后获取一个会话ID,并将含有会话ID的URL发送给目标用户。此时,当用户点击链接进入网站时,他的会话ID就变成了URL中已经含有的会话ID(攻击者已经持有的会话ID),如果用户接着在该会话期间登录网站,那么攻击者也可以登录成功。
解决这个问题的两种方法:一是同复制粘贴一样禁止在URL中内嵌会话ID,二是在登录后采用会话迁移,即当用户登录后,修改会话ID或者将之前的会话信息复制到一个新的会话中,并使之前的会话无效。
⑶ 跨站脚本和会话劫持
攻击者将利用网站的漏洞实行跨站脚本攻击,将JavaScript注入到某个页面,通过document.cookie读取会话ID cookie中的内容,当攻击者从用户处获得会话ID后,他可以通过在自己的计算机中创建cookie来模拟该会话。
解决这个问题的方法是:不要在网站中使用跨站脚本,并在所有的cookie中使用HttpOnly特性。
⑷ 不安全的cookie
最后一个需要考虑的是中间人攻击(MitM攻击),这是典型的数据截获攻击,攻击者通过观察客户端和服务端的交互或响应,从中获取信息。
解决这个问题的方法是:使用HTTPS来保护网络通信,cookie的secure标志将告诉浏览器只应该通过HTTPS传输cookie。
三、在会话中存储数据
1、在部署描述符中配置会话
下面是一份详细的关于在部署描述符中配置会话的说明:
<!--下面标签中的标签选项都是可选的,如果选择了要按照一定的顺序添加-->
<session-config>
<!--会话的超时时间(单位:分钟),Tomcat默认30分钟,如果此值小于0将永远不会过期-->
<session-timeout>30</session-timeout>
<!--只有在追踪模式中使用了cookie时,才可以使用此标签-->
<cookie-config>
<!--默认值,不需要修改-->
<name>JSESSIONID</name>
<!--domain标签和path标签对应着cookie的Domain和Path特性,Web容器已经设置了正确的默认值,通常不需要修改它们-->
<domain>example.org</domain>
<path>/shop</path>
<!--此标签内可以添加任意文本-->
<comment>other info</comment>
<!--此标签对应cookie的HttpOnly特性,默认为false,为了提高安全性,应将其设为true-->
<http-only>true</http-only>
<!--此标签对应cookie的Secure特性,默认为false,如果使用了HTTPS,应将其设为true-->
<secure>false</secure>
<!--此标签对应cookie的Max-Age特性,用于控制cookie的过期时间(单位:秒),默认没有过期时间(相当于设置为-1),
即浏览器关闭就过期。最好保持默认值不变,正常情况下也不要去使用这个标签-->
<max-age>1800</max-age>
</cookie-config>
<!--表示容器的追踪策略,可以设置一个或多个策略,从上到下安全等级递增,当使用了更高安全等级的策略就不可再使用低等级的策略-->
<tracking-mode>URL</tracking-mode>
<tracking-mode>COOKIE</tracking-mode>
<tracking-mode>SSL</tracking-mode>
</session-config>
下面是一份常用的关于在部署描述 符中配置会话的简单配置:
<!--该配置设置会话过期时间为30分钟,追踪策略为COOKIE,使用HttpOnly特性来解决安全问题,其他的将接受默认值-->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
</cookie-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
2、存储和获取数据
⑴ 在servlet中使用会话
先创建一个StoreServlet继承HttpServlet,然后添加三个方法,示例如下:
/**
* author Alex
* date 2018/10/27
* description 在servlet中创建一个简单的map,用于表示产品数据库
*/
@WebServlet(
name = "storeServlet",
urlPatterns = "/shop"
)
public class StoreServlet extends HttpServlet {
private final Map<Integer,String> products = new HashMap<>();
public StoreServlet(){
products.put(1,"桌子");
products.put(2,"椅子");
products.put(3,"床");
products.put(4,"电视");
products.put(5,"洗衣机");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getParameter("action");
if(null == action){
action = "browse";
}
switch (action){
case "addToCart":
addToCart(request,response);
break;
case "showCart":
showCart(request,response);
break;
case "browse":
default:
browse(request,response);
break;
}
}
private void showCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("products",products);
request.getRequestDispatcher("/WEB-INF/jsp/showCart.jsp").forward(request,response);
}
private void browse(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("products",products);
request.getRequestDispatcher("/WEB-INF/jsp/browse.jsp").forward(request,response);
}
private void addToCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int productId;
try {
productId = Integer.parseInt(request.getParameter("productId"));
}catch (Exception e){
e.printStackTrace();
response.sendRedirect("shop");
return;
}
HttpSession session = request.getSession();
if(null == session.getAttribute("cart")){
session.setAttribute("cart",new Hashtable<Integer,Integer>());
}
Map<Integer,Integer> cart = (Map<Integer,Integer>)session.getAttribute("cart");
if(cart.containsKey(productId)){
cart.put(productId,cart.get(productId)+1);
}else {
cart.put(productId,0);
}
response.sendRedirect("shop?action=showCart");
}
}
⑵ 在JSP中使用会话
首先,创建WEB-INF/jsp/browse.jsp文件,示例如下:
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>商品列表</title>
</head>
<body>
<h2>商品展示列表</h2>
<%
Map<Integer,String> products = (Map<Integer,String>) request.getAttribute("products");
for(Integer id:products.keySet()){
%><a href="<c:url value="/shop">
<c:param name="action" value="addToCart"/>
<c:param name="productId" value="<%=id.toString()%>"/>
</c:url>"><%=products.get(id)%><br></a><%
}
%>
<br>
<h2>
<a href="<c:url value="/shop?action=showCart"/>">购物车</a>
</h2>
</body>
</html>
然后,创建WEB-INF/jsp/showCart.jsp文件,示例如下:
<%@ page import="java.util.Map" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<html>
<head>
<title>购物车</title>
</head>
<body>
<h2>购物车展示列表</h2>
<%
Map<Integer,String> products = (Map<Integer,String>)request.getAttribute("products");
Map<Integer,Integer> cart = (Map<Integer,Integer>)session.getAttribute("cart");
if(null == cart || cart.size() ==0){
%>购物车为空<%
}else {
for(Integer id:cart.keySet()){
%>商品名:<%=products.get(id)%> 数量:<%=cart.get(id)%><br><%
}
}
%>
<br>
<h2>
<a href="<c:url value="/shop"/>">商品展示列表</a>
</h2>
</body>
</html>
3、删除数据
首先,在StoreServlet中再添加一个清空购物车的方法实现,示例如下:
private void clearCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.getSession().removeAttribute("cart");
response.sendRedirect("shop?action=showCart");
}
然后,在StoreServlet的switch中再添加一个case,示例如下:
case "clearCart":
clearCart(request,response);
break;
最后,在jsp中添加一个清除购物车的链接,示例如下:
<h2>
<a href="<c:url value="/shop?action=clearCart"/>">清空购物车</a>
</h2>
在HttpSession中有一个比较重要的方法,这就是invalidate(),当用户注销时需要调用该方法,它将销毁会话并解除所有绑定到会话的数据,即使浏览器使用相同的会话ID发起了另一个请求,已经无效的会话也不能再使用,它将会创建一个新的会话,然后响应新的会话给客户端。
4、在会话中存储更复杂一些的数据
首先,创建一个页面请求相关的实体类,用于记录页面请求的相关信息,示例如下:
/**
* author Alex
* date 2018/10/28
* description 用于记录页面请求相关参数的实体类
*/
public class PageVisit implements Serializable{
private static final long serialVersionUID = 2470743552696749915L;
//页面请求的访问时间
private Long enteredTimestamp;
//页面请求的结束时间
private Long lastTimestamp;
//请求的url和参数拼接字符串
private String request;
//请求的远程地址对象
private InetAddress ipAddress;
public Long getEnteredTimestamp() {
return enteredTimestamp;
}
public void setEnteredTimestamp(Long enteredTimestamp) {
this.enteredTimestamp = enteredTimestamp;
}
public Long getLastTimestamp() {
return lastTimestamp;
}
public void setLastTimestamp(Long lastTimestamp) {
this.lastTimestamp = lastTimestamp;
}
public String getRequest() {
return request;
}
public void setRequest(String request) {
this.request = request;
}
public InetAddress getIpAddress() {
return ipAddress;
}
public void setIpAddress(InetAddress ipAddress) {
this.ipAddress = ipAddress;
}
}
然后,创建一个处理会话活动的Servlet——ActivityServlet,示例如下:
/**
* author Alex
* date 2018/10/28
* description 用于接收记录或查看session活动的请求
*/
@WebServlet(
name = "activityServlet",
urlPatterns = "/do/*"
)
public class ActivityServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
recordSessionActivity(req);
showSessionActivity(req,resp);
}
/**
* 请求的转发
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
private void showSessionActivity(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException{
request.getRequestDispatcher("/WEB-INF/jsp/showSessionActivity.jsp").forward(request,response);
}
/**
* 获取到会话,如果会话中存在请求记录就写入最后一个请求记录的页面请求结束时间,之后向vector中添加新的请求记录
* @param request
*/
private void recordSessionActivity(HttpServletRequest request){
HttpSession session = request.getSession();
if(null == session.getAttribute("activity")){
session.setAttribute("activity",new Vector<PageVisit>());
}
Vector<PageVisit> visits = (Vector<PageVisit>) session.getAttribute("activity");
if(!visits.isEmpty()){
PageVisit lastElement = visits.lastElement();
lastElement.setLastTimestamp(System.currentTimeMillis());
}
PageVisit pageVisit = new PageVisit();
pageVisit.setEnteredTimestamp(System.currentTimeMillis());
if(null == request.getQueryString()){
pageVisit.setRequest(request.getRequestURI());
}else {
//请求参数不为空时,将参数拼接到url中
pageVisit.setRequest(request.getRequestURI()+"?"+request.getQueryString());
}
String remoteAddr = request.getRemoteAddr();
try {
InetAddress inetAddress = InetAddress.getByName(remoteAddr);
pageVisit.setIpAddress(inetAddress);
} catch (UnknownHostException e) {
e.printStackTrace();
}
visits.add(pageVisit);
}
}
最后,编写显示会话活动记录的Jsp——showSessionActivity.jsp,示例如下:
<%@ page import="java.text.DateFormat" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.Date" %>
<%@ page import="java.util.Vector" %>
<%@ page import="f1.chapter5.pojo.PageVisit" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>请求记录</title>
</head>
<body>
<%!
//声明一个处理显示时间的函数
private String showTime(long time){
String info = "";
if(time < 1000){
info = "小于1秒";
}else if(time < 60*1000){
info = time/1000 + "秒";
}else if(time < 60*60*1000){
info = time/(60*1000) + "分钟";
}
return info;
}
%>
<%
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
%>
<h2>session属性</h2>
session id :<%=session.getId()%><br>
session 是否未返回客户端:<%=session.isNew()%><br>
session 创建时间:<%=format.format(new Date(session.getCreationTime()))%><br>
<h2>session中的页面请求记录</h2>
<%
Vector<PageVisit> visits = (Vector<PageVisit>) session.getAttribute("activity");
for(PageVisit visit:visits){
%>请求的url:<%=visit.getRequest()%><%
if(null != visit.getIpAddress()){
%> 发起请求的远程IP地址:<%=visit.getIpAddress().getHostAddress()%><%
}
%> 请求访问会话的时间:<%=format.format(new Date(visit.getEnteredTimestamp()))%><%
if(null != visit.getLastTimestamp()){
%> 请求停留时间:<%=showTime(visit.getLastTimestamp()-visit.getEnteredTimestamp())%><%
}
%><br><%
}
%>
</body>
</html>
该应用程序追踪了所有的请求并在请求之间存储了它们的信息,最终显示给用户查看。
四、使用会话
1、使用会话在应用程序中添加一个简单的登录功能
首先,创建一个LoginServlet,用于控制用户的登录与注销,示例如下:
/**
* author Alex
* date 2018/10/28
* description 用于控制用户的登录和注销
*/
@WebServlet(
name = "loginServlet",
urlPatterns = "/login"
)
public class LoginServlet extends HttpServlet{
//模拟一个静态的、存在于内存之中的用户数据库
private static final Map<String,String> userMap = new Hashtable<>();
static {
userMap.put("zhangsan","zhangsan001");
userMap.put("lisi","lisi001");
userMap.put("wangwu","wangwu001");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
//如果是注销操作,则重定向到登录页面
String logout = request.getParameter("logout");
if(null != logout && logout.equals("true")){
session.invalidate();
response.sendRedirect("login");
return;
}
//用于显示登录页面
if(null == session.getAttribute("username")){
//未登录的跳转到登录页面
request.setAttribute("loginFailed",false);
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request,response);
}else {
//已经登录的跳转到商品列表页面
response.sendRedirect("shop");
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//用于验证表单提交
HttpSession session = request.getSession();
if(null == session.getAttribute("username")){
//未登录要进行验证
String username = request.getParameter("username");
String password = request.getParameter("password");
if(null == username || null == password || !userMap.containsKey(username) || !userMap.get(username).equals(password)){
//账号或密码验证错误的将loginFailed置为true
request.setAttribute("loginFailed",true);
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request,response);
}else {
session.setAttribute("username",username);
//登录成功后,改变sessionId
request.changeSessionId();
//跳转到商品列表页面
response.sendRedirect("shop");
}
}else {
//已经登录的跳转到商品列表页面
response.sendRedirect("shop");
}
}
}
然后,创建登录页面login.jsp,用于提交用户的登录账号和密码,示例如下:
<%@ page import="com.sun.org.apache.xpath.internal.operations.Bool" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>登录页面</title>
</head>
<body>
<h2>登录</h2>
<br>
<%
Boolean loginFailed = (Boolean)request.getAttribute("loginFailed");
if(loginFailed){
out.print("账号或密码错误,请重新尝试登录!");
}
%>
<br>
<form method="post" action="login">
账号:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="登录">
</form>
<br>
<h2>
<a href="login?logout=true">注销</a>
</h2>
</body>
</html>
最后,记得在browse.jsp页面也添加一个注销的链接,示例如下:
<h2>
<a href="login?logout=true">注销</a>
</h2>
2、使用监听器监测会话的变化
JavaEE中比较有用的特性之一就是会话事件,当会话发生变化时,Web容器将通知应用程序这些变化,该功能通过发布订阅模式实现,从而可以将修改会话和监听会话变化的代码解耦。而用于检测这些变化的工具被称为监听器。
现在在项目中创建一个SessionListener用于监听会话的变化,它实现了HttpSessionListener和HttpSessionIdListener,示例如下:
/**
* author Alex
* date 2018/10/28
* description 用于监测会话状态的变化
*/
@WebListener
public class SessionListener implements HttpSessionListener,HttpSessionIdListener{
/**
* 格式化时间
* @return
*/
private String showTime(){
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(new Date());
}
@Override
public void sessionIdChanged(HttpSessionEvent e, String oldSessionId) {
//当会话id改变时,添加会话活动日志
System.out.println(showTime() + " 会话id已改变,oldSessionId=" + oldSessionId + ",newSessionId=" + e.getSession().getId());
}
@Override
public void sessionCreated(HttpSessionEvent e) {
//当会话创建时,添加会话活动日志
System.out.println(showTime() + " 会话已创建,sessionId=" + e.getSession().getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent e) {
//当会话注销时,添加会话活动日志
System.out.println(showTime() + " 会话已销毁,销毁的sessionId=" + e.getSession().getId());
}
}
关于监听器的注册,可以使用注解(比较简洁),也可以在部署描述符中进行配置, 但两者不可同时使用。在web.xml中配置listener的示例如下:
<!--在部署描述符中添加listener-->
<listener>
<listener-class>f1.chapter5.listener.SessionListener</listener-class>
</listener>
注意:在启动了调试器,但在打开浏览器之前,高度窗口可能就已经出现了一条或多条日志信息,表示有一个或多个会话已经销毁。这是完全正常的,当Tomcat关闭时,它将会把会话持久到文件系统中,从而保证其中的数据不会丢失;当Tomcat重新启动时,它会尝试把这些序列化的会话恢复到内存中。如果持久化的会话过期了,那么Tomcat将通知HttpSessionListener这些会话过期了,这在WebP容器中是很标准的做法。
3、维护活跃的会话列表
除了记录会话活动,还可以在应用程序中维护一个活跃会话列表,下面来实现这个目标。首先,创建一个SessionRegistry类,使用该类模拟一个简单的数据库的增删查改,作为会话注册表。示例如下:
**
* author Alex
* date 2018/10/28
* description 会话注册表
*/
public final class SessionRegistry {
private static final Map<String,HttpSession> SESSION_MAP = new Hashtable<>();
//将构造方法设置为私有,禁止创建该对象的实例
private SessionRegistry(){}
public static void addSession(HttpSession session){
SESSION_MAP.put(session.getId(),session);
}
public static void updateSession(HttpSession session,String oldSessionId){
//添加同步块
synchronized (SESSION_MAP){
SESSION_MAP.remove(oldSessionId);
addSession(session);
}
}
public static void removeSession(HttpSession session){
SESSION_MAP.remove(session.getId());
}
public static List<HttpSession> getAllSession(){
return new ArrayList<>(SESSION_MAP.values());
}
public static int getNumberOfSession(){
return SESSION_MAP.size();
}
}
然后扩展一下SessionListener监听器的实现方法,在sessionIdChanged方法中添加如下代码:
SessionRegistry.updateSession(e.getSession(),oldSessionId);
在sessionCreated方法中添加如下代码:
SessionRegistry.addSession(e.getSession());
在sessionDestroyed方法中添加如下代码:
SessionRegistry.removeSession(e.getSession());
然后再创建SessionListServlet,用于处理展示活跃会话列表的请求,示例如下:
/**
* author Alex
* date 2018/10/28
* description 处理展示活跃会话列表的请求
*/
@WebServlet(
name = "sessionListServlet",
urlPatterns = "/sessionList"
)
public class SessionListServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
if(null == session.getAttribute("username")){
response.sendRedirect("login");
return;
}
request.setAttribute("numberOfSession", SessionRegistry.getNumberOfSession());
request.setAttribute("sessionList",SessionRegistry.getAllSession());
request.getRequestDispatcher("/WEB-INF/jsp/sessionList.jsp").forward(request,response);
}
}
最后创建sessionList.jsp页面,用于展示集合中的活跃会话列表,示例如下:
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Date" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>活跃的会话列表</title>
</head>
<body>
<h2>
<a href="login?logout=true">注销</a>
</h2>
<br>
<h2>活跃的会话列表</h2>
在这个应用程序中,现在总共有<%=request.getAttribute("numberOfSession")%>个会话存活
<br>
<%
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
List<HttpSession> sessionList = (List<HttpSession>) request.getAttribute("sessionList");
long nowTime = System.currentTimeMillis();
for(HttpSession session1:sessionList){
out.print("用户名:" + session1.getAttribute("username"));
if(session1.getId().equals(session.getId())){
out.print("(你自己)");
}
out.print(" sessionId=" + session1.getId() + ",该会话最后访问时间是:" + format.format(new Date(session1.getLastAccessedTime())));
out.print("<br>");
}
%>
</body>
</html>
五、将使用会话的应用程序集群化
1、在集群中使用会话ID
在集群中使用会话会遇到的问题是:会话是以对象的形式存在于内存中,并且只存在于Web容器的单个实例中。在负载均衡的场景中,来自同一个客户端的两个连续请求将会访问不同的Web容器,第一个Web容器将会为它收到的第一个会话请求分配会话ID,然后第二个请求将会由另一个Web容器实例进行处理,第二个实例无法识别其中的会话ID,因此将重新创建并分配一个新的会话ID,此时会话就变得无用了。
解决这个问题的方法是使用粘滞会话,粘滞会话的概念是:使负载均衡机制能感知到会话,并且总是将来自同一会话的请求发送到相同的服务器。比如在会话ID的末尾处添加一个Web服务器的标识符,用于在转发请求时转发到指定的Web服务器。
2、会话复制和故障恢复
使用粘滞会话的问题是,它可以支持扩展性,但不支持高可用性。如果创建特定会话的Tomcat实例终止服务,那么该会话将会丢失,并且用户也需要重新登录。甚至于用户可以丢失尚未保存的工作。
因此,会话应该可以在集群中复制,无论会话产生于哪个实例,它们对于所有的Web容器实例都是可用的。在应用程序中启动会话复制是很简单的,只需要在部署描述符中添加一个标签即可,示例如下:
<distributable/>
这个标签的存在就代表了Web容器将在集群中复制会话,当会话在某个实例中创建时,它将被复制到其他实例中,如果会话特性发生了变化,该会话也会被重新复制到其他实例,使得它们一直拥有最新的会话信息。