# 会话

# 会话概念

此处出自3种web会话管理的方式 (opens new window)

http是无状态的,一次请求结束,连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的。当然它知道是哪个客户端地址发过来的,但是对于我们的应用来说,我们是靠用户来管理,而不是靠客户端。所以对我们的应用而言,它是需要有状态管理的,以便服务端能够准确的知道http请求是哪个用户发起的,从而判断他是否有权限继续这个请求,这个过程就是常说的会话管理。它也可以简单理解为一个用户从登录到退出应用的一段期间。这里有3种常见的实现web应用会话管理的方式:

  1. 基于server端session的管理方式:用户第一次访问应用时,服务器给该用户创建一个sessionid,通过cookie发送给客户端,客户端后面每次访问服务器时会通过cookie发送sessionid,服务器根据sessionid来查找是哪个用户访问的。这种方式会将会话信息存储在web服务器中,占用web服务器较多内存,多台web服务器之间会有session共享问题,多个web应用共享session时,会有跨域问题。
  2. cookie-base的管理方式:上面增加了服务器的负担和架构复杂性,有人想出把用户的登录凭证直接通过cookie存到客户端,设置有效期,后续接到请求后,先解密coookie,然后做数字签名的验证,再判断凭证是否过期。这种方式实现了服务器无状态化,服务器只需要创建和验证登录Cookie即可,无需保持用户状态信息,做集群部署也相对容易。
  3. token-base的管理方式:此种方式跟上面差不多,只不过把写到cookie中的ticket写到url或header里面,它被称为token,后面服务器拿到时自己对token进行验证。这种方式和上面的方式需要注意token的刷新问题,否则到了时间用户需要重新登录。

这里主要介绍第一种会话管理方式。

# Cookie概念

由于HTTP协议是无状态的,而服务器端的业务必须是要有状态的。Cookie诞生的最初目的是为了存储web中的状态信息,以方便服务器端使用,比如判断用户是否是第一次访问网站,用户身份识别,还可以存放一些不敏感信息。它的原理是基于响应头set-cookie和请求头cookie实现,工作流程如下:

  • 服务器向客户端发送cookie
  • 浏览器将cookie保存
  • 之后的每次http请求浏览器都会将cookie发送给服务器端

Cookie的大小不会超过4KB,同一个域名下的总cookie数量也有限制(20个),一般由键值对组成。它有几个重要属性:

  1. Name/Value:设置Cookie的名称及相对应的值,对于认证Cookie,Value值包括Web服务器所提供的访问令牌。
  2. Expires属性:设置Cookie的生存期。有两种存储类型的Cookie:会话性与持久性。Expires属性缺省时,为会话性Cookie,仅保存在客户端内存中,并在用户关闭浏览器时失效;持久性Cookie会保存在用户的硬盘中,直至生存期到或用户直接在网页中单击“注销”等按钮结束会话时才会失效。
  3. Path属性:定义了Web站点上可以访问该Cookie的目录。
  4. Domain属性:指定了可以访问该 Cookie 的 Web 站点或域。Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 Domain 属性中设置.org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围。
  5. Secure属性:指定是否使用HTTPS安全协议发送Cookie。使用HTTPS安全协议,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。该方法也可用于Web站点的身份鉴别,即在HTTPS的连接建立阶段,浏览器会检查Web网站的SSL证书的有效性。但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到SSL证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。由于许多用户缺乏安全意识,因而仍可能连接到Pharming攻击所伪造的网站。
  6. HTTPOnly属性 :用于防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过XMLHTTP对象读取HTTP响应中的Set-Cookie头。

Java中servlet使用cookie方法有:

  1. 创建Cookie对象,绑定数据
    • new Cookie(String name, String value)
  2. 发送Cookie对象
    • response.addCookie(Cookie cookie)
  3. 获取Cookie,拿到数据
    • Cookie[] request.getCookies()

# Cookie细节

  1. 一次可不可以发送多个cookie? 可以创建多个Cookie对象,使用response调用多次addCookie方法发送cookie即可。
  2. cookie在浏览器中保存多长时间? 默认情况下,当浏览器关闭后,Cookie数据被销毁,可使用setMaxAge(int seconds)进行持久化存储,其中seconds值为:
    1. 正数:将Cookie数据写到硬盘的文件中进行持久化存储,并指定cookie存活时间,时间到后,cookie文件自动失效。如果时间没到,但是关闭浏览器,也不会失效。
    2. 负数:默认值,关闭浏览器后自动销毁cookie。
    3. 零:删除浏览器中的cookie信息。
  3. cookie能不能存中文? 在tomcat 8 之前 cookie中不能直接存储中文数据,此时需要将中文数据转码---一般采用URL编码(%E3);在tomcat 8之后,cookie支持中文数据,但还是不支持特殊字符,建议使用URL编码存储,URL解码解析。

# Cookie共享

  1. 假设在一个tomcat服务器中,同一个域名下,部署了多个web项目,那么在这些web项目中cookie能不能共享? 默认情况下cookie不能共享。可以使用setPath(String path)来设置cookie的获取范围。默认情况下,设置当前的虚拟目录,如果要共享,则可以将path设置为"/"。

  2. 不同的tomcat服务器间cookie共享能否共享? 如果使用setDomain(String path)设置的一级域名相同,那么多个服务器之间cookie可以共享,比如setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享。

  3. 不同的域名之间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);
    }
}
1
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的区别为:

  1. session存储数据在服务器端,Cookie存储在客户端。
  2. session没有数据大小限制,Cookie有限制。
  3. session数据安全,Cookie相对不安全。

# 基本操作

  1. 获取HttpSession对象: HttpSession session = request.getSession();
  2. 使用HttpSession对象: Object getAttribute(String name)
    void setAttribute(String name, Object value) void removeAttribute(String name)

# 注意问题

  1. 当客户端关闭后,服务器不关闭,两次获取session是否为同一个? 默认情况下不是,如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存:

    Cookie c = new Cookie("JSESSIONID",session.getId());
    c.setMaxAge(60*60);
    response.addCookie(c);
    
    1
    2
    3
  2. 客户端不关闭,服务器关闭后,两次获取的session是同一个吗? 不是同一个,但是要确保数据不丢失。tomcat会自动完成以下工作:

    • session的钝化:在服务器正常关闭之前,将session对象系列化到硬盘上。
    • session的活化:在服务器启动后,将session文件转化为内存中的session对象即可。
  3. session什么时候被销毁?

    1. 服务器关闭

    2. session对象调用invalidate()

    3. session默认失效时间为30分钟,可修改tomcat的配置:

      <session-config>
         <session-timeout>30</session-timeout>
      </session-config>
      
      1
      2
      3

# 登录案例

做一个简单的登录案例,要有验证码。

验证码部分需要注意:

  1. HTML图片单击更换新的图片时,需要增加一些时间戳作为url一部分传参,否则浏览器会视为一样的请求,并不发向服务器。
  2. 验证码验证一次成功后,在服务器需要删除该验证码,否则浏览器后退还能使用。

项目地址在,其中验证码的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());

   }

}
1
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);

      }
   }

}
1
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