Session
Session
Web服务器跟踪客户状态通常有四种方法
1.建立含有跟踪数据的隐藏字段
2.重写包含额外参数的URL
3.使用持续的Cookie
4.使用Servlet API中的Session(会话)机制
Session的概念
Session用于跟踪客户的状态。
Session指的是在一段时间内,单个客户与Web服务器的一连串相关的交互过程。
在一个Session中,客户可能会多次请求访问同一个网页,也有可能请求访问各种不同的服务器资源。
Session的例子
例1:在电子邮件应用中,从一个客户登录到电子邮件系统开始,经过收信、写信和发信等一系列操作,直至最后退出邮件系统,整个过程为一个Session。
例2:在购物网站应用中,从一个客户开始购物,到最后结账,整个过程为一个Session。
Session的运行机制
当一个Session开始时,Servlet容器将创建一个HttpSession对象,在HttpSession对象中可以存放客户状态的信息(例如购物车)。
Servlet容器为HttpSession分配一个唯一标识符,称为Session ID。
Servlet容器把Session ID作为Cookie保存在客户的浏览器中。
每次客户发出HTTP请求时,Servlet容器可以从HttpServletRequest对象中读取Session ID,然后根据Session ID找到相应的HttpSession对象,从而获取客户的状态信息。
HttpSession接口
查看Java EE的文档:javax.servlet.http.HttpSession
提供了一种方法,在用户发送多个页面请求或者访问一个网站时,用于认证用户,并且存储一些用户的信息。
Servlet容器使用HttpSession接口创建一个HTTP client和server之间的session。
这个session会持续一个特定的时间,跨越了用户的多个连接或页面请求。
一个session通常对应于一个用户,这个用户可能访问了一个网站很多次。
服务器可以用很多方法维护session,比如使用cookies或者重写URLs。
HttpSession接口允许用户:
1.查看并且操纵session信息,比如session identifier,创建时间,上次访问时间。
2.绑定objects到sessions,允许用户信息在跨越多个连接时仍保持。
当应用存储一个object到一个session,或者从session中移除一个object,session会检查这个object是否实现了HttpSessionBindingListener接口,如果实现了这个接口,servlet会通知这个object,它已经被绑定或解除绑定到这个session。通知会在绑定方法完成时被发出。对于invalidate或者expire的session,通知会在invalidate或expire结束后被发出。
一个servlet应该可以处理客户端选择不加入session的情况,比如主动关掉cookies的情况。直到客户端加入session,isNew()会返回一个true。
如果用户不加入session,getSession将会在每次请求时返回一个不同的session,isNew()永远返回true。
Session信息的范围仅对当前web应用(ServletContext),所以在一个context里存储的信息不会直接对其他context可见。
掌握HttpSession API
getId()
返回Session ID,即返回包含session唯一标识符的一个字符串。
session的标识符是由servlet容器赋予的,是依赖于实现的,不同的服务器产生标识符的算法是不一样的。
测试:新建一个jsp,body部分如下:
<body> session id = <%= session.getId() %><br> </body>
启动Tomcat,用浏览器访问,可以看到session id的值,不同的浏览器会不同,同一个浏览器刷新、开启新的窗口、新建标签等,session id都不变,但是重启浏览器之后,看到session id也会变化。
invalidate()
使当前的Session失效,Servlet容器会释放HttpSession对象占用的资源。
在刚才的测试页面加上这一句:
<body> session id = <%=session.getId()%><br> <% session.invalidate(); %> </body>
可以发现每刷新一次页面id都会变一次。每次访问页面都会产生新的session,获取id后立刻让它失效。
如果交换上面两句话,会有什么不同呢?
如下:
<body> <% session.invalidate(); %> session id = <%=session.getId()%><br> </body>
结果看起来并没有什么不同,用浏览器访问时还是每次刷新都会变换session id,这是因为每次用到的时候都生成了新的session对象。
这个invalidate()方法我们并不常用,一般是服务器用。
setAttribute(String name, Object value)和getAttribute(String name)
set方法将一对name/value键值对保存在HttpSession对象中;
get方法根据name参数返回保存在HttpSession对象中的属性值。
isNew()
判断是否是新创建的session,如果是新创建的session返回true,否则是false。
比如:
<body> session id = <%=session.getId()%><br> session isNew() = <%=session.isNew()%> </body>
第一次打开页面时为true,刷新页面后就为false。
如果客户端不知道这个session或者客户端选择不加入session,这个方法返回true。
比如,如果server只用了基于cookie的session,而客户端禁用了cookies,那么客户端每一个请求都会产生新的session。
setMaxInactiveInterval(int interval)
设定一个Session可以处于不活动状态的最大时间间隔,以秒为单位。
这里的活动是指客户端向服务器端发送请求。
如果超过这个时间,session自动失效,servlet容器会invalidate这个session,客户端会获得新的session对象。
如果设置为0或负数,表示不限制Session处于不活动状态的时间。
小于这个间隔,session不会失效,默认的间隔是30分钟。
也可以在web.xml里面配置这个时间:
<session-config> <session-timeout>20</session-timeout> </session-config>
(注意此处时间是以分钟为单位的)。
注意:程序里面的配值会覆盖掉配值文件中的值,即程序中设置的值优先级更高。
Session的生命周期
当客户第一次访问Web应用中支持Session的某个网页时,就会开始一个新的Session。
接下来当客户浏览这个Web应用的不同网页时,始终处于同一个Session中。
默认情况下,JSP网页都是支持Session的,也可以通过以下语句显式声明支持Session:
<%@ page session="true">
Session结束生命周期
在以下情况中,Session将结束生命周期,Servlet容器会将Session所占用的资源释放掉:
1.客户端关闭浏览器(真的这样吗?);
2.Session过期;
3.服务器端调用了HttpSession的invalidate()方法。
Session过期
Session过期是指当Session开始后,在一段时间内客户没有和Web服务器交互,这个Session会失效,HttpSession的setMaxInactiveInterval(int interval)方法可以设置允许Session保持不活动状态的时间(以秒为单位),如果超过这一时间,Session就会失效。
如何做到在浏览器关闭时删除session
严格地讲,做不到这一点,因为session是保存在服务器端的,客户端仅存了保存session id的Cookies,者中Cookie称为Session cookies(会话Cookie),Session cookies没有设置maxAge,即该属性为-1,表示这个cookie将会在浏览器退出时被删除。Session Cookie保存在内存里,而不是硬盘中。
再次打开浏览器时,Session Cookie没有了,发送请求时,服务器端将会生成新的Session,旧的Session被放在一边,等待过期时被删除,也即是说,服务器端会有一些没有办法再用到的旧的冗余session,静静等待死亡,所以浏览器关闭时并没有删除这些session。
可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.onclose来监视浏览器的关闭动作,然后向服务器发送一个请求来删除session。
但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力。实际上在项目中我们也不会这么做,而是让服务器在Session过期时将其删除。
实例1:邮件
登录页面:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'maillogin.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <% String username = ""; if (!session.isNew()) { username = (String) session.getAttribute("username"); if (null == username) { username = ""; } } %> <p> Session Id: <%=session.getId()%></p> <form action="mailcheck.jsp"> username: <input type="text" name="username" value="<%=username%>"> <input type="submit" value="submit"> </form> </body> </html>
登录成功页面:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'mailcheck.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <% String username = request.getParameter("username"); if (null != username) { session.setAttribute("username", username); } %> <p> Hello ! <%=username%></p> <p>你的邮箱有10封邮件</p> <a href="maillogin.jsp">转向登录</a> <a href="maillogout.jsp">注销</a> </body> </html>
注销页面:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'maillogout.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <% String username = (String) session.getAttribute("username"); session.invalidate(); %> <!-- 上面两句交换位置就会出现异常,因为session已经失效,不能获取其中属性 --> <%=username%>再见! <a href="maillogin.jsp">重新登录</a> </body> </html>
实例2:
登录页面:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'login.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <% String authority = (String) request.getAttribute("authority"); %> <form action="UserLoginServlet" method="post"> username:<input type="text" name="username" value="<%=null == request.getAttribute("username") ? "" : request .getAttribute("username")%>"><br> password:<input type="password" name="password"><br> authority: <select name="authority"> <option value="1" <%="1".equals(authority) ? "selected='selected'" : ""%>>common user</option> <option value="2" <%="2".equals(authority) ? "selected='selected'" : ""%>>administrator</option> </select> <br> <input type="submit" value="submit"> </form> </body> </html>
登录之后转到的Servlet类:
package com.mengdd.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class UserLoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); String authority = req.getParameter("authority"); HttpSession session = req.getSession(); if ("1".equals(authority)) { // 普通用户 if ("zhangsan".equals(username) && "123".equals(password)) { saveToSession(session, username, password, authority); } else { saveToRequest(req, username, password, authority); backToLogin(req, resp); } } else if ("2".equals(authority)) { // 系统管理员 if ("lisi".equals(username) && "456".equals(password)) { saveToSession(session, username, password, authority); } else { saveToRequest(req, username, password, authority); backToLogin(req, resp); } } else { // 其他情况,转回登录页面 saveToRequest(req, username, password, authority); backToLogin(req, resp); } } // 将用户信息放置到session里面 private void saveToSession(HttpSession session, String username, String password, String authority) { User user = new User(); user.setUsername(username); user.setPassword(password); user.setAuthority(authority); session.setAttribute("user", user); } private void saveToRequest(HttpServletRequest req, String username, String password, String authority) { req.setAttribute("username", username); req.setAttribute("password", password); req.setAttribute("authority", authority); } // 通过请求转发,回到登录页面 private void backToLogin(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { RequestDispatcher requestDispatcher = req .getRequestDispatcher("session/login.jsp"); requestDispatcher.forward(req, resp); } }
其中User类:
package com.mengdd.servlet; public class User { private String username; private String password; private String authority; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getAuthority() { return authority; } public void setAuthority(String authority) { this.authority = authority; } }
关于这个还可以做很多练习,就不再赘述。
通过各种实例可见,用到session时,每个JSP页面和Servlet类都要判断session是否存在,会造成很多重复代码和冗余工作,这个问题后面会介绍解决方法。
参考资料
圣思园张龙老师Java Web视频教程。