# 会话
# 会话概念
此处出自3种web会话管理的方式 (opens new window)。
http是无状态的,一次请求结束,连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的。当然它知道是哪个客户端地址发过来的,但是对于我们的应用来说,我们是靠用户来管理,而不是靠客户端。所以对我们的应用而言,它是需要有状态管理的,以便服务端能够准确的知道http请求是哪个用户发起的,从而判断他是否有权限继续这个请求,这个过程就是常说的会话管理。它也可以简单理解为一个用户从登录到退出应用的一段期间。这里有3种常见的实现web应用会话管理的方式:
- 基于server端session的管理方式:用户第一次访问应用时,服务器给该用户创建一个sessionid,通过cookie发送给客户端,客户端后面每次访问服务器时会通过cookie发送sessionid,服务器根据sessionid来查找是哪个用户访问的。这种方式会将会话信息存储在web服务器中,占用web服务器较多内存,多台web服务器之间会有session共享问题,多个web应用共享session时,会有跨域问题。
- cookie-base的管理方式:上面增加了服务器的负担和架构复杂性,有人想出把用户的登录凭证直接通过cookie存到客户端,设置有效期,后续接到请求后,先解密coookie,然后做数字签名的验证,再判断凭证是否过期。这种方式实现了服务器无状态化,服务器只需要创建和验证登录Cookie即可,无需保持用户状态信息,做集群部署也相对容易。
- token-base的管理方式:此种方式跟上面差不多,只不过把写到cookie中的ticket写到url或header里面,它被称为token,后面服务器拿到时自己对token进行验证。这种方式和上面的方式需要注意token的刷新问题,否则到了时间用户需要重新登录。
这里主要介绍第一种会话管理方式。
# Cookie概念
由于HTTP协议是无状态的,而服务器端的业务必须是要有状态的。Cookie诞生的最初目的是为了存储web中的状态信息,以方便服务器端使用,比如判断用户是否是第一次访问网站,用户身份识别,还可以存放一些不敏感信息。它的原理是基于响应头set-cookie和请求头cookie实现,工作流程如下:
- 服务器向客户端发送cookie
- 浏览器将cookie保存
- 之后的每次http请求浏览器都会将cookie发送给服务器端
Cookie的大小不会超过4KB,同一个域名下的总cookie数量也有限制(20个),一般由键值对组成。它有几个重要属性:
- Name/Value:设置Cookie的名称及相对应的值,对于认证Cookie,Value值包括Web服务器所提供的访问令牌。
- Expires属性:设置Cookie的生存期。有两种存储类型的Cookie:会话性与持久性。Expires属性缺省时,为会话性Cookie,仅保存在客户端内存中,并在用户关闭浏览器时失效;持久性Cookie会保存在用户的硬盘中,直至生存期到或用户直接在网页中单击“注销”等按钮结束会话时才会失效。
- Path属性:定义了Web站点上可以访问该Cookie的目录。
- Domain属性:指定了可以访问该 Cookie 的 Web 站点或域。Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 Domain 属性中设置.org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围。
- Secure属性:指定是否使用HTTPS安全协议发送Cookie。使用HTTPS安全协议,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。该方法也可用于Web站点的身份鉴别,即在HTTPS的连接建立阶段,浏览器会检查Web网站的SSL证书的有效性。但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到SSL证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。由于许多用户缺乏安全意识,因而仍可能连接到Pharming攻击所伪造的网站。
- HTTPOnly属性 :用于防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过XMLHTTP对象读取HTTP响应中的Set-Cookie头。
Java中servlet使用cookie方法有:
- 创建Cookie对象,绑定数据
- new Cookie(String name, String value)
- 发送Cookie对象
- response.addCookie(Cookie cookie)
- 获取Cookie,拿到数据
- Cookie[] request.getCookies()
# Cookie细节
- 一次可不可以发送多个cookie? 可以创建多个Cookie对象,使用response调用多次addCookie方法发送cookie即可。
- cookie在浏览器中保存多长时间?
默认情况下,当浏览器关闭后,Cookie数据被销毁,可使用setMaxAge(int seconds)进行持久化存储,其中seconds值为:
- 正数:将Cookie数据写到硬盘的文件中进行持久化存储,并指定cookie存活时间,时间到后,cookie文件自动失效。如果时间没到,但是关闭浏览器,也不会失效。
- 负数:默认值,关闭浏览器后自动销毁cookie。
- 零:删除浏览器中的cookie信息。
- cookie能不能存中文? 在tomcat 8 之前 cookie中不能直接存储中文数据,此时需要将中文数据转码---一般采用URL编码(%E3);在tomcat 8之后,cookie支持中文数据,但还是不支持特殊字符,建议使用URL编码存储,URL解码解析。
# Cookie共享
假设在一个tomcat服务器中,同一个域名下,部署了多个web项目,那么在这些web项目中cookie能不能共享? 默认情况下cookie不能共享。可以使用setPath(String path)来设置cookie的获取范围。默认情况下,设置当前的虚拟目录,如果要共享,则可以将path设置为"/"。
不同的tomcat服务器间cookie共享能否共享? 如果使用setDomain(String path)设置的一级域名相同,那么多个服务器之间cookie可以共享,比如setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享。
不同的域名之间cookie能否共享?
通常,cookie却不能跨越域传递,只有那些创建它的域才能访问,同一根域名下的二级域名,三级域名可以直接共享。但你可以利用重定向来间接的获取cookies,或者使用跨域资源共享CORS详解 (opens new window)。
# Cookie案例
使用cookie来判断用户是否是第一次登陆,如果不是则显示第一次登陆信息。
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
@WebServlet("/cookieTest")
public class CookieTest extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置响应的消息体的数据格式以及编码
response.setContentType("text/html;charset=utf-8");
//1.获取所有Cookie
Cookie[] cookies = request.getCookies();
boolean flag = false;//没有cookie为lastTime
//2.遍历cookie数组
if(cookies != null && cookies.length > 0){
for (Cookie cookie : cookies) {
//3.获取cookie的名称
String name = cookie.getName();
//4.判断名称是否是:lastTime
if("lastTime".equals(name)){
//有该Cookie,不是第一次访问
flag = true;//有lastTime的cookie
//设置Cookie的value
//获取当前时间的字符串,重新设置Cookie的值,重新发送cookie
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String str_date = sdf.format(date);
System.out.println("编码前:"+str_date);
//URL编码
str_date = URLEncoder.encode(str_date,"utf-8");
System.out.println("编码后:"+str_date);
cookie.setValue(str_date);
//设置cookie的存活时间
cookie.setMaxAge(60 * 60 * 24 * 30);//一个月
response.addCookie(cookie);
//响应数据
//获取Cookie的value,时间
String value = cookie.getValue();
System.out.println("解码前:"+value);
//URL解码:
value = URLDecoder.decode(value,"utf-8");
System.out.println("解码后:"+value);
response.getWriter().write("<h1>欢迎回来,您上次访问时间为:"+value+"</h1>");
break;
}
}
}
if(cookies == null || cookies.length == 0 || flag == false){
//没有,第一次访问
//设置Cookie的value
//获取当前时间的字符串,重新设置Cookie的值,重新发送cookie
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String str_date = sdf.format(date);
System.out.println("编码前:"+str_date);
//URL编码
str_date = URLEncoder.encode(str_date,"utf-8");
System.out.println("编码后:"+str_date);
Cookie cookie = new Cookie("lastTime",str_date);
//设置cookie的存活时间
cookie.setMaxAge(60 * 60 * 24 * 30);//一个月
response.addCookie(cookie);
response.getWriter().write("<h1>您好,欢迎您首次访问</h1>");
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# Session
session是服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的内存中,根据cookie中的JSESSIONID来取出其中数据。前面我们所说的会话管理模型相当于去银行取钱,我们第一次去银行取钱银行会给我们办卡,并且有一个卡号,然后我们后面可以通过该卡在银行取钱,也可以通过卡号在电子设备上取钱。在这个过程中,银行卡就相当于我们的cookie,银行卡号相当于我们的JSESSIONID,而银行的柜台就相当于我们的session,在session中我们根据客户端发回来的JSESSIONID来取出内存中对应该用户会话的对象,然后从中取出数据。
session同cookie的区别为:
- session存储数据在服务器端,Cookie存储在客户端。
- session没有数据大小限制,Cookie有限制。
- session数据安全,Cookie相对不安全。
# 基本操作
- 获取HttpSession对象: HttpSession session = request.getSession();
- 使用HttpSession对象:
Object getAttribute(String name)
void setAttribute(String name, Object value) void removeAttribute(String name)
# 注意问题
当客户端关闭后,服务器不关闭,两次获取session是否为同一个? 默认情况下不是,如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存:
Cookie c = new Cookie("JSESSIONID",session.getId()); c.setMaxAge(60*60); response.addCookie(c);
1
2
3客户端不关闭,服务器关闭后,两次获取的session是同一个吗? 不是同一个,但是要确保数据不丢失。tomcat会自动完成以下工作:
- session的钝化:在服务器正常关闭之前,将session对象系列化到硬盘上。
- session的活化:在服务器启动后,将session文件转化为内存中的session对象即可。
session什么时候被销毁?
服务器关闭
session对象调用invalidate()
session默认失效时间为30分钟,可修改tomcat的配置:
<session-config> <session-timeout>30</session-timeout> </session-config>
1
2
3
# 登录案例
做一个简单的登录案例,要有验证码。
验证码部分需要注意:
- HTML图片单击更换新的图片时,需要增加一些时间戳作为url一部分传参,否则浏览器会视为一样的请求,并不发向服务器。
- 验证码验证一次成功后,在服务器需要删除该验证码,否则浏览器后退还能使用。
项目地址在,其中验证码的servlet处理(CheckCodeServlet.java)部分为:
package SimpleLogin.service;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class CheckCodeServlet
*/
@WebServlet("/CheckCodeServlet")
public class CheckCodeServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public CheckCodeServlet() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
* response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// TODO Auto-generated method stub
this.doPost(request, response);
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
* response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// TODO Auto-generated method stub
int width = 100;
int height = 50;
// 1.创建一对象,在内存中图片(验证码图片对象)
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 2.美化图片
// 2.1 填充背景色
Graphics g = image.getGraphics();// 画笔对象
g.setColor(Color.PINK);// 设置画笔颜色
g.fillRect(0, 0, width, height);
// 2.2画边框
g.setColor(Color.BLUE);
g.drawRect(0, 0, width - 1, height - 1);
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789";
// 生成随机角标
Random ran = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 4; i++) {
int index = ran.nextInt(str.length());
// 获取字符
char ch = str.charAt(index);// 随机字符
sb.append(ch);
// 2.3写验证码
g.drawString(ch + "", width / 5 * i, height / 2);
}
String checkCode_session = sb.toString();
// 将验证码存入session
request.getSession().setAttribute("checkCode_session", checkCode_session);
// 2.4画干扰线
g.setColor(Color.GREEN);
// 随机生成坐标点
for (int i = 0; i < 10; i++) {
int x1 = ran.nextInt(width);
int x2 = ran.nextInt(width);
int y1 = ran.nextInt(height);
int y2 = ran.nextInt(height);
g.drawLine(x1, y1, x2, y2);
}
// 3.将图片输出到页面展示
ImageIO.write(image, "jpg", response.getOutputStream());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
用户登录的处理逻辑为(LoginServlet.java):
package SimpleLogin.service;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.beanutils.BeanUtils;
import SimpleLogin.dao.UserDao;
import SimpleLogin.domain.User;
/**
* Servlet implementation class LoginServlet
*/
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public LoginServlet() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
* response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// TODO Auto-generated method stub
response.getWriter().append("Served at: ").append(request.getContextPath());
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
* response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// TODO Auto-generated method stub
// 1.设置request编码
request.setCharacterEncoding("utf-8");
// 2.获取参数
// String username = request.getParameter("username");
// String password = request.getParameter("password");
// String checkCode = request.getParameter("checkCode");
Map<String, String[]> map = request.getParameterMap();
Set<String> keyset = map.keySet();
for (String name : keyset) {
//获取键获取值
String[] values = map.get(name);
System.out.println(name);
for (String value : values) {
System.out.println(value);
}
System.out.println("-----------------");
}
User loginUser = new User();
try {
BeanUtils.populate(loginUser,map);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// 3.先获取生成的验证码
HttpSession session = request.getSession();
String checkCode_session = (String) session.getAttribute("checkCode_session");
// 删除session中存储的验证码
session.removeAttribute("checkCode_session");
// 3.先判断验证码是否正确
if (checkCode_session != null && checkCode_session.equalsIgnoreCase(loginUser.getCheckCode())) {
// 忽略大小写比较验证码,如果正确则判断用户名和密码是否一致
//if ("admin".equals(username) && "123".equals(password)) {// 需要调用UserDao查询数据库
UserDao userDao = new UserDao();
User user = userDao.login(loginUser);
if(user != null) {
// 登录成功
// 存储信息,用户信息
session.setAttribute("user", user.getUsername());
// 重定向到success.jsp
response.sendRedirect(request.getContextPath() + "/success.jsp");
} else {
// 登录失败
// 存储提示信息到request
request.setAttribute("login_error", "用户名或密码错误");
// 转发到登录页面
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
} else {
// 验证码不一致
// 存储提示信息到request
request.setAttribute("cc_error", "验证码错误");
// 转发到登录页面
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114