什么是Session
Session 是另一种记录浏览器状态的机制。不同的是Cookie保存在浏览器中,Session保存在服务器中。用户使用浏览器访问服务器的时候,服务器把用户的信息以某种的形式记录在服务器,这就是Session
如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”。
为什么要使用Session技术?
Session比Cookie使用方便,Session可以解决Cookie解决不了的事情【Session可以存储对象,Cookie只能存储字符串。】。
Session API
- long getCreationTime();【获取Session被创建时间】
- String getId();【获取Session的id】
- long getLastAccessedTime();【返回Session最后活跃的时间】
- ServletContext getServletContext();【获取ServletContext对象】
- void setMaxInactiveInterval(int var1);【设置Session超时时间】
- int getMaxInactiveInterval();【获取Session超时时间】
-
Object getAttribute(String var1);【获取Session属性】
- Enumeration
- void setAttribute(String var1, Object var2);【设置Session属性】
- void removeAttribute(String var1);【移除Session属性】
- void invalidate();【销毁该Session】
- boolean isNew();【该Session是否为新的】
Session作为域对象
从上面的API看出,Session有着request和ServletContext类似的方法。其实Session也是一个域对象。Session作为一种记录浏览器状态的机制,只要Session对象没有被销毁,Servlet之间就可以通过Session对象实现通讯
- 我们来试试吧,在Servlet4中设置Session属性
//得到Session对象
HttpSession httpSession = request.getSession();
//设置Session属性
httpSession.setAttribute("name", "看完博客就要点赞!!");
- 在Servlet5中获取到Session存进去的属性
//获取到从Servlet4的Session存进去的值
HttpSession httpSession = request.getSession();
String value = (String) httpSession.getAttribute("name");
System.out.println(value);

- 一般来讲,当我们要存进的是用户级别的数据就用Session,那什么是用户级别呢?只要浏览器不关闭,希望数据还在,就使用Session来保存。
Session的生命周期和有效期
<session-config>
<session-timeout>20</session-timeout>
</session-config>

-
第二种方式:在单个的web.xml文件中设置,对单个web应用有效,如果有冲突,以自己的web应用为准。
<session-config>
<session-timeout>20</session-timeout>
</session-config>
-
第三种方式:通过setMaxInactiveInterval()方法设置
//设置Session最长超时时间为60秒,这里的单位是秒
httpSession.setMaxInactiveInterval(60);
System.out.println(httpSession.getMaxInactiveInterval());


使用Session完成简单的购物功能
- 我们还是以书籍为例,所以可以copy“显示浏览过的商品“例子部分的代码。
response.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = response.getWriter();
printWriter.write("网页上所有的书籍:" + "<br/>");
//拿到数据库所有的书
LinkedHashMap<String, Book> linkedHashMap = DB.getAll();
Set<Map.Entry<String, Book>> entry = linkedHashMap.entrySet();
//显示所有的书到网页上
for (Map.Entry<String, Book> stringBookEntry : entry) {
Book book = stringBookEntry.getValue();
String url = "/ouzicheng/Servlet6?id=" + book.getId();
printWriter.write(book.getName());
printWriter.write("<a href='" + url + "'>购买</a>");
printWriter.write("<br/>");
}
- 在购物车页面上,获取到用户想买的书籍【用户可能不单想买一本书,于是乎,就用一个List容器装载书籍】,有了:先遍历Cookie,再判断是否是第一次访问Servlet的逻辑思路,我们就可以先获取到Session的属性,如果Session的属性为null,那么就是还没有该属性
//得到用户想买书籍的id
String id = request.getParameter("id");
//根据书籍的id找到用户想买的书
Book book = (Book) DB.getAll().get(id);
//获取到Session对象
HttpSession httpSession = request.getSession();
//由于用户可能想买多本书的,所以我们用一个容器装着书籍
List list = (List) httpSession.getAttribute("list");
if (list == null) {
list = new ArrayList();
//设置Session属性
httpSession.setAttribute("list",list);
}
//把书籍加入到list集合中
list.add(book);
- 按我们正常的逻辑思路:先创建一个ArrayList对象,把书加到list集合中,然后设置Session的属性。这样是行不通的。每次Servlet被访问的时候都会创建一个ArrayList集合,书籍会被分发到不同的ArrayList中去。所以下面的代码是不行的!
//得到用户想买书籍的id
String id = request.getParameter("id");
//根据书籍的id找到用户想买的书
Book book = (Book) DB.getAll().get(id);
//获取到Session对象
HttpSession httpSession = request.getSession();
//创建List集合
List list = new ArrayList();
list.add(book);
httpSession.setAttribute("list", list);
- 既然用户已经购买了书籍,那么也应该给提供页面显示用户购买过哪些书籍
//得到用户想买书籍的id
String id = request.getParameter("id");
//根据书籍的id找到用户想买的书
Book book = (Book) DB.getAll().get(id);
//获取到Session对象
HttpSession httpSession = request.getSession();
//由于用户可能想买多本书的,所以我们用一个容器装着书籍
List list = (List) httpSession.getAttribute("list");
if (list == null) {
list = new ArrayList();
//设置Session属性
httpSession.setAttribute("list",list);
}
//把书籍加入到list集合中
list.add(book);
String url = "/ouzicheng/Servlet7";
response.sendRedirect(url);
//要得到用户购买过哪些书籍,得到Session的属性遍历即可
HttpSession httpSession = request.getSession();
List<Book> list = (List) httpSession.getAttribute("list");
if (list == null || list.size() == 0) {
printWriter.write("对不起,你还没有买过任何商品");
} else {
printWriter.write("您购买过以下商品:");
printWriter.write("<br/>");
for (Book book : list) {
printWriter.write(book.getName());
printWriter.write("<br/>");
}
}

Session的实现原理
- 用现象说明问题,我在Servlet4中的代码设置了Session的属性
//得到Session对象
HttpSession httpSession = request.getSession();
//设置Session属性
httpSession.setAttribute("name", "看完博客就要点赞!!");
- 接着在Servlet7把Session的属性取出来
String value = (String) request.getSession().getAttribute("name");
printWriter.write(value);
- 自然地,我们能取到在Servlet4中Session设置的属性

- 接着,我在浏览器中新建一个会话,再次访问Servlet7



- 当我们用同一个浏览器访问Servlet7的时候,浏览器会把Cookie的值通过http协议带过去给服务器,服务器就知道用哪一Session。

- 而当我们使用新会话的浏览器访问Servlet7的时候,该新浏览器并没有Cookie,服务器无法辨认使用哪一个Session,所以就获取不到值
浏览器禁用了Cookie,Session还能用吗?
上面说了Session是依靠Cookie来识别用户浏览器的。如果我的用户浏览器禁用了Cookie了呢?绝大多数的手机浏览器都不支持Cookie,那我的Session怎么办?

- 好的,我们来看看情况是怎么样的。用户浏览器访问Servlet4的时候,服务器向用户浏览器颁发了一个Cookie

- 但是呢,当用户浏览器访问Servlet7的时候,由于我们禁用了Cookie,所以用户浏览器并没有把Cookie带过去给服务器。

一看,Session好像不能用了。但是Java Web提供了解决方法:URL地址重写
- HttpServletResponse类提供了两个URL地址重写的方法:
- encodeURL(String url)
- encodeRedirectURL(String url)
需要值得注意的是:这两个方法会自动判断该浏览器是否支持Cookie,如果支持Cookie,重写后的URL地址就不会带有jsessionid了【当然了,即使浏览器支持Cookie,第一次输出URL地址的时候还是会出现jsessionid(因为没有任何Cookie可带)】
下面我们就以上面“购物”的例子来做试验吧!首先我们来看看禁用掉Cookie对原来的小例子有什么影响。
访问Servlet1,随便点击一本书籍购买



String url = "/ouzicheng/Servlet7";
response.sendRedirect(response.encodeURL(url));
- 再次访问Servlet1,当我点击javaweb的时候,已经能够成功出现我买过的商品了。并且Session的id通过URL地址重写,使用的是同一个Session


- URL地址重写的原理:将Session的id信息重写到URL地址中。服务器解析重写后URL,获取Session的id。这样一来,即使浏览器禁用掉了Cookie,但Session的id通过服务器端传递,还是可以使用Session来记录用户的状态。
Session禁用Cookie
Java Web规范支持通过配置禁用Cookie
- 禁用自己项目的Cookie
- 在META-INF文件夹下的context.xml文件中修改(没有则创建)

<?xml version='1.0' encoding='utf-8'?>
<Context path="/ouzicheng" cookies="false">
</Context>
- 禁用全部web应用的Cookie

注意:该配置只是让服务器不能自动维护名为jsessionid的Cookie,并不能阻止Cookie的读写。
Session案例
使用Session完成用户简单登陆
private String username = null;
private String password = null;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
....各种set、get方法
private static List<User> list = new ArrayList<>();
//装载些数据进数据库
static {
list.add(new User("aaa","111"));
list.add(new User("bbb","222"));
list.add(new User("ccc","333"));
}
//通过用户名和密码查找用户
public static User find(String username, String password) {
for (User user : list) {
if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
return user;
}
}
return null;
}
- 表单提交的工作我就在jsp写了,如果在Servlet写太麻烦了!
<form action="/ouzicheng/LoginServlet" method="post">
用户名:<input type="text" name="username"><br/>
密码:<input type="password" name="password"><br/>
<input type="submit" value="提交">
</form>
-
获取到表单提交的数据,查找数据库是否有相对应的用户名和密码。如果没有就提示用户名或密码出错了,如果有就跳转到另外一个页面
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = UserDB.find(username, password);
//如果找不到,就是用户名或密码出错了。
if (user == null) {
response.getWriter().write("you can't login");
return;
}
//标记着该用户已经登陆了!
HttpSession httpSession = request.getSession();
httpSession.setAttribute("user", user);
//跳转到其他页面,告诉用户成功登陆了。
response.sendRedirect(response.encodeURL("index.jsp"));
- 我们来试试下数据库没有的用户名和密码,提示我不能登陆。




利用Session防止表单重复提交
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>表单提交</title>
<script type="text/javascript">
//定义一个全局标识量:是否已经提交过表单数据
var isCommitted = false;
function doSubmit() {
//false表示的是没有提交过,于是就可以让表单提交给Servlet
if(isCommitted==false) {
isCommitted = true;
return true;
}else {
return false;
}
}
</script>
</head>
<body>
<form action="/ouzicheng/Servlet7" onsubmit="return doSubmit()">
用户名:<input type="text" name="username">
<input type="submit" value="提交">
</form>
</body>
</html>
- 好的,我们来试一下是不是真的可以解决网络延迟所造成的多次提交表单数据,注意鼠标,我已经点击过很多次的了!

<script type="text/javascript">
function doSubmit() {
var button = document.getElementById("button");
button.disabled = disabled;
return true;
}
</script>

/*
* 产生随机数就应该用一个对象来生成,这样可以避免随机数的重复。
* 所以设计成单例
* */
public class TokenProcessor {
private TokenProcessor() {
}
private final static TokenProcessor TOKEN_PROCESSOR = new TokenProcessor();
public static TokenProcessor getInstance() {
return TOKEN_PROCESSOR;
}
public static String makeToken() {
//这个随机生成出来的Token的长度是不确定的
String token = String.valueOf(System.currentTimeMillis() + new Random().nextInt(99999999));
try {
//我们想要随机数的长度一致,就要获取到数据指纹
MessageDigest messageDigest = MessageDigest.getInstance("md5");
byte[] md5 = messageDigest.digest(token.getBytes());
//如果我们直接 return new String(md5)出去,得到的随机数会乱码。
//因为随机数是任意的01010101010,在转换成字符串的时候,会查gb2312的码表,gb2312码表不一定支持该二进制数据,得到的就是乱码
//于是乎经过base64编码成了明文的数据
BASE64Encoder base64Encoder = new BASE64Encoder();
return base64Encoder.encode(md5);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}
//生出随机数
TokenProcessor tokenProcessor = TokenProcessor.getInstance();
String token = tokenProcessor.makeToken();
//将随机数存进Session中
request.getSession().setAttribute("token", token);
//跳转到显示页面
request.getRequestDispatcher("/login.jsp").forward(request, response);
<form action="/ouzicheng/Servlet7" >
用户名:<input type="text" name="username">
<input type="submit" value="提交" id="button">
<%--使用EL表达式取出session中的Token--%>
<input type="hidden" name="token" value="${token}" >
</form>
- 在处理表单提交页面中判断:jsp隐藏域是否有值带过来,Session中的值是否为空,Session中的值和jsp隐藏域带过来的值是否相等
String serverValue = (String) request.getSession().getAttribute("token");
String clientValue = request.getParameter("token");
if (serverValue != null && clientValue != null && serverValue.equals(clientValue)) {
System.out.println("处理请求");
//清除Session域对象数据
request.getSession().removeAttribute("token");
}else {
System.out.println("请不要重复提交数据!");
}
- 下面我们再来看一下,已经可以解决表单重复提交的问题了!

实现原理是非常简单的:
- 在session域中存储一个token
- 然后前台页面的隐藏域获取得到这个token
- 在第一次访问的时候,我们就判断seesion有没有值,如果有就比对。对比正确后我们就处理请求,接着就把session存储的数据给删除了
- 等到再次访问的时候,我们session就没有值了,就不受理前台的请求了!
一次性校验码
//在内存中生成图片
BufferedImage bufferedImage = new BufferedImage(80, 20, BufferedImage.TYPE_INT_RGB);
//获取到这张图片
Graphics2D graphics2D = (Graphics2D) bufferedImage.getGraphics();
//设置背景色为白色
graphics2D.setColor(Color.white);
graphics2D.fillRect(0, 0, 80, 20);
//设置图片的字体和颜色
graphics2D.setFont(new Font(null, Font.BOLD, 20));
graphics2D.setColor(Color.BLUE);
//生成随机数
String randomNum = makeNum();
//往这张图片上写数据,横坐标是0,纵坐标是20
graphics2D.drawString(randomNum, 0, 20);
//将随机数存进Session域中
request.getSession().setAttribute("randomNum", randomNum);
//控制浏览器不缓存该图片
response.setHeader("Expires", "-1");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
//通知浏览器以图片的方式打开
response.setHeader("Content-type", "image/jpeg");
//把图片写给浏览器
ImageIO.write(bufferedImage, "jpg", response.getOutputStream());
private String makeNum() {
Random random = new Random();
//生成0-6位的随机数
int num = random.nextInt(999999);
//验证码的数位全都要6位数,于是将该随机数转换成字符串,不够位数就添加
String randomNum = String.valueOf(num);
//使用StringBuffer来拼凑字符串
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 6 - randomNum.length(); i++) {
stringBuffer.append("0");
}
return stringBuffer.append(randomNum).toString();
}
<form action="/ouzicheng/Login2Servlet">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
验证码:<input type="text" name="randomNum">
<img src="/ouzicheng/ImageServlet" ><br><br>
<input type="submit" value="提交">
</form>
- 处理提交表单数据的Servlet,判断用户带过来验证码的数据是否和Session的数据相同。
//获取用户输入验证码的数据
String client_randomNum = request.getParameter("randomNum");
//获取Session中的数据
String session_randomNum = (String) request.getSession().getAttribute("randomNum");
//判断他俩数据是否相等,用户是否有输入验证码,Session中是否为空
if (client_randomNum == null || session_randomNum == null || !client_randomNum.equals(session_randomNum)) {
System.out.println("验证码错误了!!!");
return ;
}
//下面就是验证用户名和密码...................


对于校验码实现思路是这样子的:
- 使用awt语法来描写一张验证码,生成随机数保存在seesion域中,我们让验证码不能缓存起来【做到验证码都不一样】
- 页面直接访问Servlet来获取我们的验证码,于是我们验证码的值就会改变【同时session的值也会被改变】
- 当用户验证的时候,就是session内的值的验证了。
Session和Cookie的区别
-
从存储方式上比较
- Cookie只能存储字符串,如果要存储非ASCII字符串还要对其编码。
- Session可以存储任何类型的数据,可以把Session看成是一个容器
-
从隐私安全上比较
-
Cookie存储在浏览器中,对客户端是可见的。信息容易泄露出去。如果使用Cookie,最好将Cookie加密
-
Session存储在服务器上,对客户端是透明的。不存在敏感信息泄露问题。
-
从有效期上比较
- Cookie保存在硬盘中,只需要设置maxAge属性为比较大的正整数,即使关闭浏览器,Cookie还是存在的
- Session的保存在服务器中,设置maxInactiveInterval属性值来确定Session的有效期。并且Session依赖于名为JSESSIONID的Cookie,该Cookie默认的maxAge属性为-1。如果关闭了浏览器,该Session虽然没有从服务器中消亡,但也就失效了。
-
从对服务器的负担比较
- Session是保存在服务器的,每个用户都会产生一个Session,如果是并发访问的用户非常多,是不能使用Session的,Session会消耗大量的内存。
- Cookie是保存在客户端的。不占用服务器的资源。像baidu、Sina这样的大型网站,一般都是使用Cookie来进行会话跟踪。
-
从浏览器的支持上比较
- 如果浏览器禁用了Cookie,那么Cookie是无用的了!
- 如果浏览器禁用了Cookie,Session可以通过URL地址重写来进行会话跟踪。
-
从跨域名上比较
- Cookie可以设置domain属性来实现跨域名
- Session只在当前的域名内有效,不可夸域名
Cookie和Session共同使用
如果仅仅使用Cookie或仅仅使用Session可能达不到理想的效果。这时应该尝试一下同时使用Session和Cookie
- 那么,什么时候才需要同时使用Cookie和Session呢?
- 在上一篇博客中,我们使用了Session来进行简单的购物,功能也的确实现了。现在有一个问题:我在购物的途中,不小心关闭了浏览器。当我再返回进去浏览器的时候,发现我购买过的商品记录都没了!!为什么会没了呢?原因也非常简单:服务器为Session自动维护的Cookie的maxAge属性默认是-1的,当浏览器关闭掉了,该Cookie就自动消亡了。当用户再次访问的时候,已经不是原来的Cookie了。
- 我们现在想的是:即使我不小心关闭了浏览器了,我重新进去网站,我还能找到我的购买记录。
要实现该功能也十分简单,问题其实就在:服务器为Session自动维护的Cookie的maxAge属性是-1,Cookie没有保存在硬盘中。我现在要做的就是:把Cookie保存在硬盘中,即使我关闭了浏览器,浏览器再次访问页面的时候,可以带上Cookie,从而服务器识别出Session。
第一种方式:只需要在处理购买页面上创建Cookie,Cookie的值是Session的id返回给浏览器即可
Cookie cookie = new Cookie("JSESSIONID",session.getId());
cookie.setMaxAge(30*60);
cookie.setPath("/ouzicheng/");
response.addCookie(cookie);

如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章的同学,可以关注微信公众号:Java3y
更多的文章可往:文章的目录导航