为了更好的理解Cookie和Session,我们通过一个简单的登录案例来讲解,印象会更加深刻。
image.png

登录HTML页面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录</title>
  6. </head>
  7. <body>
  8. 欢迎登录,请输入用户名、密码以及图形验证码
  9. <form method="POST" action="/Demo5/login">
  10. <p>姓名:<input type="text" name="name"></input></p>
  11. <p>密码:<input type="password" name="password"></input></p>
  12. <p>图形验证码:<input type="text" name="code"></input> <img src="/Demo5/captcha"></p>
  13. <p><input type="submit" value="确定"></input></p>
  14. </form>
  15. </body>
  16. </html>

首先搞定图形验证码

图形验证码的由来

维基百科了解一下
还有一篇文章比较通俗的讲解了为什么使用图形验证码https://www.dingxiang-inc.com/blog/post/399

概况起来有以下几个方面:

1、为了防止机器冒充人类做暴力破解:暴力破解想想就恐怖,这关系每个用户的网络安全,现在很多网站、APP都绑定用户的银行账户,有很多内容还涉及到个人隐私,如果被不法分子暴力破解,那损失可就大了。
2、防止大规模在线注册滥用服务:很多机友肯定都很讨厌那些恶意注册灌水的,满满一屏全是恶意评论和广告,瞬间没有好心情;
3、防止滥用在线批量化操作:比如在投票的时候,有些恶意刷票软件就可以实现批量化投票功能,想想自己辛苦拉票,人家一键就搞定?
4、防止自动发布:比如早些年黑客们写一串代码就肆无忌惮地朝网络上倾倒大量的、无意义的僵尸信息,垃圾邮件、垃圾广告、垃圾评论到处乱飞。污染了网络环境的同时,更有甚者被广告诈骗。
5、防止信息被大量采集聚合:互联网时代,最有价值的就是内容生产,精心创作的原创文章,一秒被爬取?肝颤啊。

重点:图形验证码是在服务器端生成,返回到客户端展示(如果客户端生成,服务端怎么进行验证对比)
实现方式:
实现方式有很多种,咱们使用Google的kaptcha库来生成验证吗(听说验证码最初来源于Google的一个项目)。Maven依赖

  1. <dependency>
  2. <groupId>com.github.penggle</groupId>
  3. <artifactId>kaptcha</artifactId>
  4. <version>2.3.2</version>
  5. </dependency>

使用 kaptcha 可以方便的配置如下属性:
4w6pjwzih4.png

实战操作一下

新建Maven web项目,pom.xml中配置上面依赖,在项目的resources目录下新建kaptcha.properties配置文件

  1. # 验证码长度
  2. kaptcha.textproducer.char.length=4
  3. # 字体
  4. kaptcha.textproducer.font.names=微软雅黑
  5. # 图片边框
  6. kaptcha.border=yes
  7. # 边框颜色
  8. kaptcha.border.color=105,179,90
  9. # 字体颜色
  10. kaptcha.textproducer.font.color=blue
  11. # 图片宽
  12. kaptcha.image.width=120
  13. # 图片高
  14. kaptcha.image.height=50
  15. # 字体大小
  16. kaptcha.textproducer.font.size=30
  17. # session key
  18. kaptcha.session.key=code

该文件用于配置图形验证码的样式信息。

Servlet类

  1. import com.google.code.kaptcha.impl.DefaultKaptcha;
  2. import com.google.code.kaptcha.util.Config;
  3. import javax.imageio.ImageIO;
  4. import javax.servlet.*;
  5. import javax.servlet.http.*;
  6. import javax.servlet.annotation.*;
  7. import java.awt.image.BufferedImage;
  8. import java.io.IOException;
  9. import java.io.InputStream;
  10. import java.util.Properties;
  11. @WebServlet("/captcha")
  12. public class CaptchaServlet extends HttpServlet {
  13. @Override
  14. protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  15. // 创建Katpcha对象
  16. DefaultKaptcha dk = new DefaultKaptcha();
  17. // 验证码的配置信息,宽高、字体等
  18. try (InputStream is = getClass().getClassLoader().getResourceAsStream("kaptcha.properties")) {
  19. Properties properties = new Properties();
  20. properties.load(is);
  21. Config config = new Config(properties);
  22. dk.setConfig(config);
  23. }
  24. // 生成验证码字符串
  25. String code = dk.createText();
  26. // 根据字符串生成验证码图片
  27. BufferedImage image = dk.createImage(code);
  28. // 设置返回数据的格式,重要
  29. response.setContentType("image/jpeg");
  30. // 图片数据返回到客户端
  31. ImageIO.write(image, "jpg", response.getOutputStream());
  32. }
  33. }

上面的操作已经完成图形验证码的展示啦。

遇到的问题

接下来我们需要输入用户名和密码以及图形验证码,发送登录请求给服务端,服务端拿到用户名和密码从数据库里面查询是否有该用户,还要比对一下图形验证码是否正确,但是图形验证码怎么比较呢?服务端怎么知道是谁或者哪个客户端发送的验证码呢?有一种思路是这样的,由于图形验证码是服务端生成的,在生成的时候可以先保存起来,根据不同的客户端保存不同的图形验证码,再根据不同的客户端发送的登录请求找到不同的图形验证码进行比较。但问题是服务端怎么区分是谁发送的登录请求呢?为了更好的理解这个场景,我们的登录界面发送了两个请求
Servlet Cookie和Session - 图3

由于HTTP是无状态的,即服务端不知道用户上次做了什么。也就是发送验证码请求和登录请求是两个完全独立的行为,互相也不知道对放的存在。现在我们想解决的问题是,要让这两个请求产生联系,产生怎样的联系?当发送图形验证码请求的时候,返回图形验证码图片的同时,如果再返回一个标记(唯一标识),客户端把这个标记存起来(用于标记是哪个客户端)。当客户端点击登录按钮发送登录请求的时候,把标记也发送给服务端,服务端拿到这个标记,就知道是哪个客户端发送的登录请求,服务端根据不同的标记取出对应的验证码,再和登录接口中的图形验证码进行比较,这样就完美解决啦。
由于这种类似的场景比较多,比如

  • 服务器想识别多个请求是否来自同一个客户端
  • 来自同一个客户端的多个请求之间共享数据

业界为了解决这个问题,出现了一种新的技术叫会话跟踪技术。在Java中实现会话跟踪技术的常用方案就是Cookie和Session,由于Session是基于Cookie来实现的,我们首先了解一下Cookie的内容。

Cookie

维基百科了解一下MDN以及JavaScript.info对Cookie做了比较详细的介绍,值得反复阅读。下面对这两个网站比较重要的信息,更加通俗的梳理讲解。

Cookie的重要点

  • 保存在客户端的一小块数据,每个cookie大小不能超过 4KB,因此不能保存大的东西
  • document.cookie 的值由 name=value 对组成,以 ; 分隔。每一个都是独立的 cookie。
    1. document.cookie = "user=John"; // 只会更新名称为 user 的 cookie
    创建Cookie可以通过JavaScript的方式也可以通过服务端http响应头(设置Set-Cookie)的方式创建
    先看JavaScript的方式创建,我们搞了一个设置cookie的页面
    image.png
    对应的HTML页面 ```html <!DOCTYPE html> 学习cookie

    键:

    值:

    过期时间:

  1. 创建cookie可以使用如下方式创建
  2. ```html
  3. document.cookie = "name=John"; // 只会更新名称为name的cookie

此代码设置了一个名称为name值为John的cookie。
document.cookie= 操作不会重写所有 cookie。只会更新写入其中提到的 cookie,而不会涉及其他 cookie。
由于cookie的键和值可能是任意字符,比如中文等。我们需要对其进行编码,使用内建的 encodeURIComponent 函数对其进行编码:

  1. document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(John);

cookie重要的选项

默认情况下关闭浏览器cookie就会消失,我们可以使用expires 或者 max-age,设置过期时间。

最简单的就是使用max-age
max-age指明 cookie 的过期时间距离当前时间的秒数。

  1. // cookie 会在一小时后失效
  2. document.cookie = "name=John; max-age=3600";

Cookie的作用域

Domain 和 Path 标识定义了Cookie的作用域:即允许 Cookie 应该发送给哪些URL。
Domain 属性
Domain 指定了哪些主机可以接受 Cookie。如果不指定,默认为 origin,不包含子域名。如果指定了Domain,则一般包含子域名。因此,指定 Domain 比省略它的限制要少。
比如访问site.com站点设置了cookie,默认情况下,只有再次访问site.com站点时,cookie才会自动通过请求头发送cookie(自动发送,不需要我们手动发送,HTTP协议规定的)。如果我们想访问其子域名如forum.site.com 也自动上报其cookie,就需要设置Domain选项了。

  1. // 使 cookie 可以被在任何子域 *.site.com 访问:
  2. document.cookie = "user=John; domain=site.com"

path=/mypath
url 路径前缀,该路径下的页面可以访问该 cookie。必须是绝对路径。默认为当前路径。
如果一个 cookie 带有 path=/admin 设置,那么该 cookie 在 /admin 和 /admin/something 下都是可见的,但是在 /home 或 /adminpage 下不可见。
通常,我们应该将 path 设置为根目录:path=/,以使 cookie 对此网站的所有页面可见。

服务器设置cookie

cookie通常有web服务器通过响应头Set-Cookie设置的

  1. HTTP/1.1 200 OK
  2. Date: Fri, 04 Feb 2000 21:03:38 GMT
  3. Server: Apache/1.3.9 (UNIX) PHP/4.0b3
  4. Set-Cookie: name=xyz; expires=Friday, 04-Feb-07 22:03:38 GMT;
  5. path=/; domain=runoob.com
  6. Connection: close
  7. Content-Type: text/html

我们通过实例演练一下。

通过 Servlet 设置 Cookie

(1) 创建一个 Cookie 对象

  1. Cookie cookie = new Cookie("name","Join");//创建一个名为name 值为Join的cookie


(2) 设置最大生存周期,您可以使用 setMaxAge 方法来指定 cookie 能够保持有效的时间(以秒为单位)。

  1. cookie.setMaxAge(60*60*24);//最长有效期为 24 小时

(3) 发送 Cookie 到 HTTP 响应头:您可以使用 response.addCookie 来添加 HTTP 响应头中的 Cookie

  1. response.addCookie(cookie);

服务端是通过响应头的方式,告诉客户端存cookie的
代码如下:

  1. import javax.servlet.*;
  2. import javax.servlet.http.*;
  3. import javax.servlet.annotation.*;
  4. import java.io.IOException;
  5. @WebServlet("/setCookieServlet")
  6. public class SetCookieServlet extends HttpServlet {
  7. @Override
  8. protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  9. // (1) 创建一个 Cookie 对象
  10. Cookie cookie = new Cookie("name","lifufa");
  11. // (2) 设置最大生存周期,您可以使用 setMaxAge 方法来指定 cookie 能够保持有效的时间(以秒为单位)。
  12. cookie.setMaxAge(60*60*24);//最长有效期为 24 小时
  13. cookie.setPath("/");//当前域名下的所有路径有效
  14. // (3) 发送 Cookie 到 HTTP 响应头:您可以使用 response.addCookie 来添加 HTTP 响应头中的 Cookie
  15. response.addCookie(cookie);
  16. }
  17. }

�当再次访问该域名下的其他请求时,在请求头里面会自动携带以前设置过的Cookie,也就是客户端通过请求头把cookie带给服务端的。
image.png
cookie是通过请求时的请求头到达服务端的,服务端是通过响应时的响应头到达客户端的。

通过 Servlet 读取 Cookie

要读取 Cookie,您需要通过调用 HttpServletRequestgetCookies( ) 方法创建一个 javax.servlet.http.Cookie 对象的数组。然后循环遍历数组,并使用 getName() 和 getValue() 方法来访问每个 cookie 和关联的值。

  1. Cookie[] cookies = request.getCookies();
  2. for (Cookie cookie : cookies) {
  3. System.out.println(cookie.getName());
  4. System.out.println(cookie.getValue());
  5. }

通过 Servlet 删除 Cookie

步骤如下:

  • 读取一个现有的 cookie,并把它存储在 Cookie 对象中。
  • 使用 setMaxAge() 方法设置 cookie 的年龄为零,来删除现有的 cookie。
  • 把这个 cookie 添加到响应头。 ```java Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if (cookie.getName().equals(“age”)){ //删除名称为age的cookie
    1. cookie.setMaxAge(0);
    2. response.addCookie(cookie);
    } }
  1. <a name="XiRn8"></a>
  2. ## Session
  3. 为了跟踪用户状态,服务器可以向浏览器分配一个唯一ID,并以Cookie的形式发送到浏览器,浏览器在后续访问时总是附带此Cookie,这样,服务器就可以识别用户身份。我们把这种基于唯一ID识别用户身份的机制称为Session。
  4. <a name="k0PiA"></a>
  5. ### 为什么需要Session
  6. 使用 Cookie 有一个非常大的局限,就是如果 Cookie 很多,则无形的增加了客户端与服务端的数据传输量。而且由于浏览器对 Cookie 数量的限制(每个域的 cookie 总数不得超过 20+ 左右,具体限制取决于浏览器),注定我们不能再 Cookie 中保存过多的信息,于是 Session 出现。
  7. <a name="zJfpG"></a>
  8. ### Session的作用
  9. Session 的作用就是在服务器端保存一些用户的数据,然后传递给用户一个名字为JSESSIONID 的 Cookie,这个 JESSIONID 对应这个服务器中的一个 Session 对象(Session的唯一标识),通过它就可以获取到保存用户信息的 Session。
  10. 现在回到我们的登录案例,当发送图形验证码请求时,服务器端应该将验证码存在Session中。
  11. ```java
  12. import com.google.code.kaptcha.impl.DefaultKaptcha;
  13. import com.google.code.kaptcha.util.Config;
  14. import javax.imageio.ImageIO;
  15. import javax.servlet.*;
  16. import javax.servlet.http.*;
  17. import javax.servlet.annotation.*;
  18. import java.awt.image.BufferedImage;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.util.Properties;
  22. @WebServlet("/captcha")
  23. public class CaptchaServlet extends HttpServlet {
  24. @Override
  25. protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  26. // 创建Katpcha对象
  27. DefaultKaptcha dk = new DefaultKaptcha();
  28. // 验证码的配置信息,宽高、字体等
  29. try (InputStream is = getClass().getClassLoader().getResourceAsStream("kaptcha.properties")) {
  30. Properties properties = new Properties();
  31. properties.load(is);
  32. Config config = new Config(properties);
  33. dk.setConfig(config);
  34. }
  35. // 生成验证码字符串
  36. String code = dk.createText();
  37. HttpSession session = request.getSession();
  38. session.setAttribute("code",code.toLowerCase()); //保存到Session中,并忽略大小写
  39. System.out.println(code.toLowerCase());
  40. // 根据字符串生成验证码图片
  41. BufferedImage image = dk.createImage(code);
  42. // 设置返回数据的格式,重要
  43. response.setContentType("image/jpeg");
  44. // 图片数据返回到客户端
  45. ImageIO.write(image, "jpg", response.getOutputStream());
  46. }
  47. }

当发起登录请求时。

  1. import javax.servlet.ServletException;
  2. import javax.servlet.annotation.WebServlet;
  3. import javax.servlet.http.Cookie;
  4. import javax.servlet.http.HttpServlet;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.http.HttpServletResponse;
  7. import java.io.IOException;
  8. @WebServlet("/login")
  9. public class LoginServlet extends HttpServlet {
  10. @Override
  11. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  12. // 1. 判断图形验证码是否正确
  13. String code = (String) req.getSession().getAttribute("code");
  14. if (code!=null){
  15. code = code.toLowerCase();
  16. }
  17. if (code.equals(req.getParameter("code"))){
  18. System.out.println("验证码验证成功!");
  19. }else {
  20. System.out.println("验证码错误!");
  21. }
  22. // 2. 查询用户名密码是否在数据库中。。。
  23. }
  24. @Override
  25. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  26. doGet(req,resp);
  27. }
  28. }

调用request.getSession()代码的原理。

首先检查客户端是否发送一个叫JSESSIONID的cookie,如果没有创建一个新的session,并且这个session会有一个id。这个id可以通过session.getId()获取,这个session会保存在服务器的内存中。在响应的时候会在响应头中加一个名为JSESSIONID,值为session的id的cookie。类似下面的代码

  1. 1.创建session对象
  2. 2.获取sessionID,每一个session都有一个唯一的ID
  3. String sessionId = session.getId();
  4. 3.sessionId存储到名字为JSESSIONIDcookie
  5. Cookie cookie = new Cookie("JSESSIONID", sessionId);
  6. 设置cookie的有效路径
  7. cookie.setPath(request.getContextPath());
  8. 4.response.addCookie(cookie);

如果有发送一个JSESSIONID的cookie,就直接返回id为JSESSIONID的cookie。

Servlet Cookie和Session - 图6
这样就能办到同一个客户端发送的请求都对应同一个session,因为只要是同一个客户端cookie中的JSESSIONID=123是同一个,到服务端后拿到的是同一个session。而不同的客户端session肯定是不同的因为JSESSIONID不同,这样就区分了客户端。

优点

开发相对简单,不需要编写太多代码就能完成开发,比如浏览器会自动携带Cookie发送给服务端。

缺点

这种方式有很大的弊端

  • 默认不支持非浏览器环境,比如移动端,因为没有浏览器环境,当然也可以手动维护cookie,每次请求把JSESSIONID放在请求头里面,这样就比较麻烦
  • 不支持分布式架构,分布式架构需要解决共享session的问题。

Session的有效期

默认情况下是30分钟,可以在web.xml中配置

  1. <session-config>
  2. <session-timeout>30</session-timeout> 单位:分钟
  3. <session-config>

关于JSESSIONID

默认情况下,浏览器关闭cookie中的JSESSIONID就会销毁。
如果想延长JSESSIONID的寿命,可以手动的设置JSESSIONID的时间。

  1. Cookie cookie = new Cookie("JSESSIONID",session.getId());
  2. cookie.setMaxAge(60*60*24);最长有效期为 24 小时
  3. response.addCookie(cookie);