我只想卷死各位,或者被各位卷死,在此特别感谢黑马程序员的JavaWeb教程


本文摘要:

  • 理解什么是会话跟踪技术
  • 掌握Cookie的使用
  • 掌握Session的使用
  • 完善用户登录注册案例的功能

会话跟踪技术概述

概述

对于会话跟踪这四个字,我们需要拆开来进行解释,首先要理解什么是会话,然后再去理解什么是会话跟踪

  • 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应

    • 从浏览器发出请求到服务端响应数据给前端之后,一次会话(在浏览器和服务器之间)就被建立了
    • 会话被建立之后,如果浏览器或服务端都没有被关闭,则会话会持续建立着
    • 浏览器和服务器就可以继续使用该会话进行请求发送和响应,上述的整个过程就被称之为会话
      用实际场景来理解一下会话,比如在我们访问京东的时候,当打开浏览器进入京东首页之后,浏览器和京东服务器之间就建立了一次会话,后面的搜索商品,查看商品详情,加入购物车等,都是在这一次会话中完成的。
  • 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求之间共享数据

    • 服务器会收到多个请求,这多个请求可能来自多个浏览器(京东这种购物网站,肯定时时刻刻都有好多人访问)
    • 服务器需要用来识别请求是否来自同一个浏览器
    • 服务器用来识别浏览器的过程,这个过程就是会话跟踪
    • 服务器识别浏览器后就可以在同一个会话中的多次请求之间来共享数据

那么我们又有一个问题需要思考,一个会话中的多次请求为什么要共享数据呢?有了这个数据共享功能之后,能实现哪些功能呢?

  • 购物车: 加入购物车去购物车结算是两次请求,但是后面这次请求要想展示前一次请求所添加的商品,就需要用到数据共享
  • 页面展示用户登录信息:很多网站,登录后访问多个功能发送多次请求后,浏览器上都会有当前登录用户的信息[用户名],登录百度账号,访问贴吧,网盘,都不需要再登陆一次
  • 网站登录页面的记住我功能:当用户登录成功后,勾选记住我按钮后下次再登录的时候,网站就会自动填充用户名和密码,简化用户的登录操作,多次登录就会有多次请求,他们之间也涉及到共享数据
  • 登录页面的验证码功能:生成验证码和输入验证码点击注册这也是两次请求,这两次请求的数据之间要进行对比,相同则允许注册,不同则拒绝注册,该功能的实现也需要在同一次会话中共享数据。

那为什么现在浏览器和服务器不支持数据共享呢?

  • 浏览器和服务器之间使用的是HTTP请求来进行数据传输
  • HTTP协议是无状态的,每次浏览器向服务器请求时,服务器都会将该请求视为新的请求
  • HTTP协议设计成无状态的目的是让每次请求之间相互独立,互不影响
  • 请求与请求之间独立后,就无法实现多次请求之间的数据共享

分析完具体的原因后,那么该如何实现会话跟踪技术呢? 具体的实现方式有:

  1. 客户端会话跟踪技术:Cookie
  2. 服务端会话跟踪技术:Session

这两个技术都可以实现会话跟踪,它们之间最大的区别:Cookie是存储在浏览器端而Session是存储在服务器端

小结

这里主要介绍了下什么是会话和会话跟踪技术,需要注意的是:

  • HTTP协议是无状态的,靠HTTP协议是无法实现会话跟踪
  • 想要实现会话跟踪,就需要用到Cookie和Session

学习Cookie,我们主要解决下面几个问题:

  • 什么是Cookie?
  • Cookie如何来使用?
  • Cookie是如何实现的?
  • Cookie的使用注意事项有哪些?

Cookie的基本使用

概念

Cookie:客户端会话技术,将数据保存到客户端,以后每次请求都携带Cookie数据进行访问。

Cookie的工作流程

  • 服务端提供了两个Servlet,分别是AServlet和BServlet
  • 浏览器发送HTTP请求1给服务端,服务端AServlet接收请求并进行业务处理
  • 服务端AServlet在处理请求的过程中可以创建一个Cookie对象,并将name=zs的数据存入Cookie
  • 服务端AServlet在响应数据的时候,会把Cookie对象响应给浏览器
  • 浏览器接收到响应数据,会把Cookie对象中的数据存储在浏览器内存中,此时浏览器和服务端就建立了一次会话
  • 在同一次会话中,浏览器再次发送HTTP请求2给服务端BServlet,浏览器会鞋带Cookie对象中的所有数据
  • BServlet接收到请求和数据后,就可以获取到存储在Cookie对象中的数据,这样同一个会话中的多次请求之间就实现了数据共享

Cookie的基本使用

对于Cookie的使用,我们更关注的应该是后台代码如何操作Cookie,对于Cookie的操作主要分两大类,分别是发送Cookie获取Cookie

  • 发送Cookie
    • 创建Cookie对象,并设置数据
    1
    Cookie cookie = new Cookie("key","value");
    • 发送Cookie到客户端:使用response对象
    1
    response.addCookie(cookie);

介绍完发送Cookie对应的步骤后,接下面通过一个案例来完成Cookie的发送

需求:在Servlet中生成Cookie对象并存入数据,然后将数据发送给浏览器

  1. 创建Maven项目,项目名称为cookie-demo,并在pom.xml添加依赖
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
  1. 编写Servlet类,名称为AServlet
1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 在AServlet中创建Cookie对象,存入数据,发送给前端
1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie cookie = new Cookie("username","zs");
response.addCookie(cookie);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 启动测试,在浏览器查看Cookie对象中的值

名称 username
内容 zs
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2022年8月20日星期六 10:47:16
到期时间 浏览会话结束时

  • 获取Cookie
    • 获取客户端携带的所有Cookie,使用request对象
    1
    Cookie[] cookies = request.getCookies();
    • 遍历数组,获取每一个Cookie对象,使用Cookie对象方法获取数据
    1
    2
    3
    4
    5
    for (Cookie cookie : cookies) {
    String name = cookie.getName();
    String value = cookie.getValue();
    System.out.println(name + ":" + value);
    }

介绍完获取Cookie对应的步骤后,接下面再通过一个案例来完成Cookie的获取
需求:在Servlet中获取前一个案例存入在Cookie对象中的数据

  1. 编写一个新Servlet类,名称为BServlet
1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
String name = cookie.getName();
if ("username".equals(name)) {
String value = cookie.getValue();
System.out.println(name + ":" + value);
}
}
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 启动测试,在控制台打印出获取的值
    成功输出 username:zs

小结

这里主要讲解了Cookie的基本使用,包含两部分内容

  • 发送Cookie:
    • 创建Cookie对象,并设置值:Cookie cookie = new Cookie("key","value");
    • 发送Cookie到客户端使用的是Reponse对象:response.addCookie(cookie);
  • 获取Cookie:
    • 使用Request对象获取Cookie数组:Cookie[] cookies = request.getCookies();
    • 遍历数组
    • 获取数组中每个Cookie对象的值:cookie.getName()cookie.getValue()

Cookie的原理分析

对于Cookie的实现原理是基于HTTP协议的,其中设计到HTTP协议中的两个请求头信息:

  • 响应头:set-cookie
  • 请求头: cookie
  • 前面的案例中已经能够实现,AServlet给前端发送Cookie,BServlet从request中获取Cookie的功能
  • 对于AServlet响应数据的时候,Tomcat服务器都是基于HTTP协议来响应数据
  • 当Tomcat发现后端要返回的是一个Cookie对象之后,Tomcat就会在响应头中添加一行数据Set-Cookie:username=zs
  • 浏览器获取到响应结果后,从响应头中就可以获取到Set-Cookie对应值username=zs,并将数据存储在浏览器的内存中
  • 浏览器再次发送请求给BServlet的时候,浏览器会自动在请求头中添加Cookie: username=zs发送给服务端BServlet
  • Request对象会把请求头中cookie对应的值封装成一个个Cookie对象,最终形成一个数组
  • BServlet通过Request对象获取到Cookie[]后,就可以从中获取自己需要的数据

Cookie的使用细节

Cookie的存活时间

名称 username
内容 zs
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2022年8月20日星期六 10:47:16
到期时间 浏览会话结束时

刚刚我们在浏览器中查看Cookie对象中的值时,发现Cookie还有到期时间,这个到期时间是浏览会话结束时,那我们关闭浏览器之后,再打开进行访问,BServlet就不能获取到Cookie数据了。

因为默认情况下,Cookie存储在浏览器内存中,当浏览器关闭,内存释放,则Cookie被销毁

但是如果使用这种默认情况下的Cookie,有些需求就无法实现,比如我们登录一些网站的时候

  • 第一次输入用户名和密码并勾选记住我然后进行登录
  • 下次再登录的时候,用户名和密码就会被自动填充,不需要再重新输入登录
  • 比如记住我这个功能需要记住用户名和密码一个星期,那么使用默认情况下的Cookie就会出现问题
  • 因为默认情况,浏览器一关,Cookie就会从浏览器内存中删除,对于记住我功能就无法实现

所以我们现在就遇到一个难题是如何将Cookie持久化存储?
Cookie其实已经为我们提供好了对应的API来完成这件事,这个API就是setMaxAge,

  • 设置Cookie存活时间
1
setMaxAge(int seconds);

参数值为:

  1. 正数:将Cookie写入浏览器所在电脑的硬盘,持久化存储。到时间自动删除
  2. 负数:默认值,Cookie在当前浏览器内存中,当浏览器关闭,则Cookie被销毁
  3. 零:删除对应Cookie

接下来,我们就在AServlet中去设置Cookie的存活时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie cookie = new Cookie("username", "zs");
cookie.setMaxAge(7 * 24 * 60 * 60); //设置七天的
response.addCookie(cookie);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}

修改完代码后,启动测试

  • 访问AServlet后,把浏览器关闭重启后访问BServlet,能在控制台打印出username:zs,说明Cookie没有随着浏览器关闭而被销毁
  • 通过浏览器查看Cookie的内容,到期时间成功续期7天

名称 username
内容 zs
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2022年8月20日星期六 10:47:16
到期时间 2022年8月27日星期六 10:47:16

Cookie存储中文

Cookie是不能直接存储中文的,但是如果有这方面的需求,这个时候该如何解决呢?
我们可以使用URL编码,所以如果需要存储中文,就需要进行转码,具体的实现思路为:

  1. 在AServlet中对中文进行URL编码,采用URLEncoder.encode(),将编码后的值存入Cookie中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String value = "张三";
//URL编码
value = URLEncoder.encode(value, "UTF-8");
Cookie cookie = new Cookie("username", value);
cookie.setMaxAge(7 * 24 * 60 * 60);
response.addCookie(cookie);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 在BServlet中获取Cookie中的值,获取的值为URL编码后的值(%E5%BC%A0%E4%B8%89

名称 username
内容 %E5%BC%A0%E4%B8%89
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2022年8月20日星期六 12:25:37
到期时间 2022年8月27日星期六 12:25:37

  1. 将获取的值在进行URL解码,采用URLDecoder.decode(),就可以获取到对应的中文值,控制台输出username:张三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
String name = cookie.getName();
if ("username".equals(name)) {
String value = cookie.getValue();
//解码
value = URLDecoder.decode(value, "utf-8");
System.out.println(name + ":" + value);
}
}
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}

Session

Session的基本使用

概念

Session:服务端会话跟踪技术:将数据保存到服务端。

  • Session是存储在服务端而Cookie是存储在客户端
  • 存储在客户端的数据容易被窃取和截获,存在很多不安全的因素
  • 存储在服务端的数据相比于客户端来说就更安全

工作流程

  • 在服务端的AServlet获取一个Session对象,把数据存入其中
  • 在服务端的BServlet获取到相同的Session对象,从中取出数据
  • 就可以实现一次会话中多次请求之间的数据共享了
  • 现在最大的问题是如何保证AServlet和BServlet使用的是同一个Session对象(后面的Session原理会说)

基本使用

在JavaEE中提供了HttpSession接口,来实现一次会话的多次请求之间数据共享功能。

具体的使用步骤为:

  • 获取Session对象,使用的是request对象
1
HttpSession session = request.getSession();
  • Session对象提供的功能:
    • 存储数据到 session 域中
      1
      void setAttribute(String name, Object o)
    • 根据 key,获取值
      1
      Object getAttribute(String name)
    • 根据 key,删除该键值对
      1
      void removeAttribute(String name)

介绍完Session相关的API后,接下来通过一个案例来完成对Session的使用

需求:在一个Servlet中往Session中存入数据,在另一个Servlet中获取Session中存入的数据

  1. 创建名为SessionDemo1的Servlet类:获取Session对象、存储数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取Session对象
HttpSession session = request.getSession();
//存储数据
session.setAttribute("username","ls");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 创建名为SessionDemo2的Servlet类:获取Session对象、获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取Session对象
HttpSession session = request.getSession();
//获取数据
Object username = session.getAttribute("username");
//看看能否输出到控制台
System.out.println(username);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 启动测试
    先访问SessionDemo1,将数据存入Session,再访问SessionDemo2,从Session中获取数据,控制台成功输出ls

小结

至此Session的基本使用就已经完成了,重点要掌握的是:

  • Session的获取
    1
    HttpSession session = request.getSession();
  • Session常用方法的使用
    1
    2
    void setAttribute(String name, Object o)
    Object getAttribute(String name)

注意:Session中可以存储的是一个Object类型的数据,也就是说Session中可以存储任意数据类型。

介绍完Session的基本使用之后,那么Session的底层到底是如何实现一次会话两次请求之间的数据共享呢?

Session的原理分析

Session是基于Cookie实现的

这句话其实不太能详细的说明Session的底层实现,接下来,我们一步步来分析下Session的具体实现原理:

  • 前提条件
    • Session要想实现一次会话多次请求之间的数据共享,就必须要保证多次请求获取Session的对象是同一个。
  • 如何验证
    • 在上面案例中的两个ServletDemo中分别打印下Session对象,比较其地址值是否相同
1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
System.out.println(session);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
System.out.println(session);
Object username = session.getAttribute("username");
System.out.println(username);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  • 启动测试,分别访问/demo1/demo2,输出如下

org.apache.catalina.session.StandardSessionFacade@69186990
org.apache.catalina.session.StandardSessionFacade@69186990

  • 通过打印可以得到如下结论:

    • 两个Servlet类中获取的Session对象是同一个
    • 把demo1和demo2请求刷新多次,控制台最终打印的结果都是同一个
  • 那么问题又来了,如果新开一个浏览器,访问demo1或者demo2,打印在控制台的Session还是同一个对象么?

    • 测试的结果:打印的Session不一样。

    org.apache.catalina.session.StandardSessionFacade@1c7eb9a
    org.apache.catalina.session.StandardSessionFacade@1c7eb9a

  • 所以Session实现的也是一次会话中的多次请求之间的数据共享。

    • 那么最主要的问题就来了,Session是如何保证在一次会话中获取的Session对象是同一个呢?
  1. demo1在第一次获取session对象的时候,session对象会有一个唯一的标识,假如是id:10
  2. demo1在session中存入其他数据并处理完成所有业务后,需要通过Tomcat服务器响应结果给浏览器
  3. Tomcat服务器发现业务处理中使用了session对象,就会把session的唯一标识id:10当做一个cookie,添加Set-Cookie:JESSIONID=10到响应头中,并响应给浏览器
  4. 浏览器接收到响应结果后,会把响应头中的coookie数据存储到浏览器的内存中
  5. 浏览器在同一会话中访问demo2的时候,会把cookie中的数据按照cookie: JESSIONID=10的格式添加到请求头中并发送给服务器Tomcat
  6. demo2获取到请求后,从请求头中就读取cookie中的JSESSIONID值为10,然后就会到服务器内存中寻找id:10的session对象,如果找到了,就直接返回该对象,如果没有则新创建一个session对象
  7. 关闭打开浏览器后,因为浏览器的cookie已被销毁,所以就没有JESSIONID的数据,服务端获取到的session就是一个全新的session对象

至此,Session是基于Cookie来实现的这就话,我们就解释完了

小结

介绍完Session的原理,我们只需要记住

  • Session是基于Cookie来实现的

Session的使用细节

Session的钝化与活化

首先需要大家思考的问题是:

  • 服务器重启后,Session中的数据是否还在?

要想回答这个问题,我们首先应该想到

  1. 服务器端AServlet和BServlet共用的session对象应该是存储在服务器的内存中
  2. 服务器重新启动后,内存中的数据应该是已经被释放,对象也应该都销毁了

所以session数据应该也已经不存在了。但是如果session不存在会引发什么问题呢?
举个例子说明下,

  1. 用户把需要购买的商品添加到购物车,因为要实现同一个会话多次请求数据共享,所以假设把数据存入Session对象中
  2. 用户正要付钱的时候接到一个电话,付钱的动作就搁浅了
  3. 正在用户打电话的时候,购物网站因为某些原因需要重启
  4. 重启后session数据被销毁,购物车中的商品信息也就会随之而消失
  5. 用户想再次发起支付,就会出为问题

所以说对于session的数据,我们应该做到就算服务器重启了,也应该能把数据保存下来才对。

那么Tomcat服务器在重启的时候,session数据到底会不会保存以及是如何保存的,我们可以通过实际案例来演示下:

  1. 先启动Tomcat服务器
  2. 访问/demo1将数据存入session中
  3. Ctrl+C正确停止Tomcat服务器
  4. 再次重新启动Tomcat服务器
  5. 访问/demo2 查看是否能获取到session中的数据

经过测试,会发现只要服务器是正常关闭和启动,session中的数据是可以被保存下来的。

那么Tomcat服务器到底是如何做到的呢?

具体的原因就是:Session的钝化和活化:

  • 钝化:在服务器正常关闭后,Tomcat会自动将Session数据写入硬盘的文件中

    • 钝化的数据路径为:项目目录\target\tomcat\work\Tomcat\localhost\项目名称\SESSIONS.ser
  • 活化:再次启动服务器后,从文件中加载数据到Session中

    • 数据加载到Session中后,路径中的SESSIONS.ser文件会被删除掉

对于上述的整个过程,都是Tomcat自己完成的,不需要我们参与,了解一下即可。

######## 小结

Session的钝化和活化介绍完后,需要我们注意的是:

  • session数据存储在服务端,服务器重启后,session数据会被保存
  • 浏览器被关闭启动后,重新建立的连接就已经是一个全新的会话,获取的session数据也是一个新的对象
  • session的数据要想共享,浏览器不能关闭,所以session数据不能长期保存数据
  • cookie是存储在客户端,是可以长期保存

Session的销毁

Session的销毁会有两种方式:

  • 默认情况下,无操作,30分钟自动销毁
    • 对于这个失效时间,是可以通过配置进行修改的
      • 在项目的web.xml中配置
      • 如果没有配置,默认是30分钟,默认值是在Tomcat的web.xml配置文件中写死的
  • 调用Session对象的invalidate()进行销毁
    • 在SessionDemo2类中添加session销毁的方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @WebServlet("/demo2")
    public class SessionDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    HttpSession session = request.getSession();
    //销毁
    session.invalidate();
    Object username = session.getAttribute("username");
    System.out.println(username);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
    • 启动访问测试,先访问demo1将数据存入到session,再次访问demo2从session中获取数据,会得到如下错误信息
      java.lang.IllegalStateException: getAttribute: Session already invalidated
    • 该销毁方法一般会在用户退出的时候,需要将session销毁掉。

Cookie和Session小结

Cookie 和 Session 都是来完成一次会话内多次请求间数据共享的。所需两个对象放在一块,就需要思考:

Cookie和Session的区别是什么?

  • 区别:
    • 存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端
    • 安全性:Cookie不安全,Session安全
    • 数据大小:Cookie最大3KB,Session无大小限制
    • 存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
    • 服务器性能:Cookie不占服务器资源,Session占用服务器资源

Cookie和Session的应用场景分别是什么?

  • 应用场景:
    • 购物车:使用Cookie来存储
    • 以登录用户的名称展示:使用Session来存储
    • 记住我功能:使用Cookie来存储
    • 验证码:使用session来存储

结论

  • Cookie是用来保证用户在未登录情况下的身份识别
  • Session是用来保存用户登录后的数据

用户登录注册案例

需求分析

在上篇文章,我们已经完成了对品牌数据的增删改查操作,现在我们继续完善登录功能

需求说明:

  1. 完成用户登录功能,如果用户勾选"记住用户" ,则下次访问登录页面自动填充用户名密码
  2. 完成注册功能,并实现验证码功能

用户登录功能

  1. 需求:

    • 用户登录成功后,跳转到列表页面,并在页面上展示当前登录的用户名称
    • 用户登录失败后,跳转回登录页面,并在页面上展示对应的错误信息
  2. 实现流程分析

    • 前端通过表单发送请求和数据给Web层的LoginServlet
    • 在LoginServlet中接收请求和数据[用户名和密码]
    • LoginServlet接收到请求和数据后,调用Service层完成根据用户名和密码查询用户对象
    • 在Service层需要编写UserService类,在类中实现login方法,方法中调用Dao层的UserMapper
    • 在UserMapper接口中,声明一个根据用户名和密码查询用户信息的方法
    • Dao层把数据查询出来以后,将返回数据封装到User对象,将对象交给Service层
    • Service层将数据返回给Web层
    • Web层获取到User对象后,判断User对象,如果为Null,则将错误信息响应给登录页面,如果不为Null,则跳转到列表页面,并把当前登录用户的信息存入Session携带到列表页面。
  3. 具体实现

    • db1数据库下新建tb_user
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    -- 创建用户表
    CREATE TABLE tb_user(
    id int primary key auto_increment,
    username varchar(20) unique,
    password varchar(32)
    );

    -- 添加数据
    INSERT INTO tb_user(username,password) values('zhangsan','123'),('lisi','234');

    SELECT * FROM tb_user;
    • com.blog.pojo下新建User类
      根据数据表中的数据类型创建即可
    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
    package com.blog.pojo;

    public class User {

    private Integer id;
    private String username;
    private String password;

    public Integer getId() {
    return id;
    }

    public void setId(Integer id) {
    this.id = id;
    }

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

    @Override
    public String toString() {
    return "User{" +
    "id=" + id +
    ", username='" + username + '\'' +
    ", password='" + password + '\'' +
    '}';
    }
    }
    • com.blog.mapper包下新建UserMapper接口
      我们只需要登录,添加(注册)和查询(检测用户名是否已被占用)这三个功能
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface UserMapper {
    @Select("select * from tb_user where username=#{username} and password=#{password}")
    User login(@Param("username") String username, @Param("password") String password);

    @Select("select * from tb_user where username=#{username}")
    User selectByUsername(String username);

    @Insert("insert into tb_user values (null,#{username},#{password})")
    void add(User user);
    }
    • resources/com/itheima/mapper目录下新建UserMapper.xml
      由于我们使用注解开发,所以这里啥也不用写,当SQL语句很复杂的时候,我们才会采用此种方式
    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.blog.mapper.UserMapper">

    </mapper>
    • com.itheima.service包下,创建UserService类
      这里要实现登录的逻辑,上一篇文章也练习过很多遍了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class UserService {
    private SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();

    public User login(String username, String password) {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.login(username, password);
    sqlSession.close();
    return user;
    }
    }
    • 写一个登录的页面login.jsp
      我这里就不搞那些花里胡哨的了,平平淡淡才是真,主要实现的是后端的逻辑,前端暂时无所谓了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@ page isELIgnored="false" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <form action="/brand_demo/loginServlet" method="post">
    <h1>登录系统</h1>
    用户名:<input name="username" type="text"><br>
    密码:<input name="password" type="password"><br>
    记住账号:<input name="remember" type="checkbox" value="1"><br>
    <input value="登录" type="submit">
    <!--这里提前写了一下注册的跳转链接,后面我们会把这个页面也写完-->
    <a href="/brand_demo/register.jsp">没有账号?</a>
    </form>
    </body>
    </html>
    • 创建LoginServlet类
      • 实现登录功能,我们要先获取用户输入的用户名和密码
      • 然后调用service查询User
      • 判断User是否为null
    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
    @WebServlet("/loginServlet")
    public class LoginServlet extends HttpServlet {
    private UserService userService = new UserService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //获取用户名和密码
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    //调用service查询User
    User user = userService.login(username, password);
    if (user != null) {
    //登陆成功,跳转到查询所有BrandServlet
    HttpSession session = request.getSession();
    //将user的信息存储到session域中,便于在brand.jsp页面添加提示信息
    session.setAttribute("user", user);
    response.sendRedirect("/brand_demo/selectAllServlet");
    } else {
    //登陆失败,将错误信息存储到request域中
    request.setAttribute("login_msg", "用户名或密码错误");
    //并跳转到login.jsp
    request.getRequestDispatcher("/login.jsp").forward(request, response);
    }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
    • 登陆成功,由于我们将user的信息存储到了session域中,我们就可以在brand.jsp页面上添加提示信息<h1>${user.username},欢迎你</h1>
      当我们登陆成功后,会在页面的最上方用h1标题显示username,欢迎你

    • 登陆失败,由于我们将错误提示语句存储到了request域中,所以我们可以在login.jsp中用EL表达式接收一下错误信息,并展示到页面上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <form action="/brand_demo/loginServlet" method="post">
    <h1>登录系统</h1>
    <div>${login_msg}</div> <!--当登陆失败的时候,会在这里显示"用户名或密码错误"-->
    用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
    密码:<input name="password" type="password" value="${cookie.password.value}"><br>
    记住账号:<input name="remember" type="checkbox" value="1"><br>
    <input value="登录" type="submit">
    <a href="/brand_demo/register.jsp">没有账号?</a>
    </form>
    • 启动,访问测试,输入错误账号密码,会显示用户名或密码错误,输入正确的账号密码,会跳转到brand.jsp页面,展示所有商品信息,同时页面最上方有username,欢迎你字样

    • 小结

      • 在LoginServlet中,将登录成功的用户数据存入session中,方法在列表页面中获取当前登录用户信息进行展示
      • 在LoginServlet中,将登录失败的错误信息存入到request中,如果存入到session中就会出现这次会话的所有请求都有登录失败的错误信息,这个是不需要的,所以不用存入到session中

记住我-设置Cookie

  1. 需求:
    如果用户勾选"记住用户" ,则下次访问登陆页面自动填充用户名密码。这样可以提升用户的体验。
    对应上面这个需求,最大的问题就是: 如何自动填充用户名和密码?

  2. 实现流程分析

    • 因为记住我功能要实现的效果是,就算用户把浏览器关闭过几天再来访问也能自动填充,所以需要将登陆信息存入一个可以长久保存,并且能够在浏览器关闭重新启动后依然有效的地方,就是我们前面讲的Cookie,所以:
      • 将用户名和密码写入Cookie中,并且持久化存储Cookie,下次访问浏览器会自动携带Cookie
      • 在页面获取Cookie数据后,设置到用户名和密码框中
      • 何时写入Cookie?
        • 用户必须登陆成功后才需要写
        • 用户必须在登录页面勾选了记住我的复选框
  3. 具体实现

  • 在LoginServlet获取复选框的值并在登录成功后进行设置Cookie
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
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
//这里要多获取一个复选框的值,在之前的jsp代码中,我将复选框的值设为了"1"
String remember = request.getParameter("remember");
User user = userService.login(username, password);
if (user != null) {
//当登录成功后,我们再来判断一下复选框选了没
if ("1".equals(remember)) {
//创建Cookie对象
Cookie c_username = new Cookie("username", username);
//设置一下存活时间为7天,更长也行,随便你
c_username.setMaxAge(7 * 24 * 60 * 60);
Cookie c_password = new Cookie("password", password);
c_password.setMaxAge(7 * 24 * 60 * 60);
//发送Cookie
response.addCookie(c_username);
response.addCookie(c_password);
}
HttpSession session = request.getSession();
session.setAttribute("user", user);
response.sendRedirect("/brand_demo/selectAllServlet");
} else {
request.setAttribute("login_msg", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
  • 启动访问测试,只有当前用户名和密码输入正确,并且勾选了Remeber的复选框,用F12打开开发者工具,监测网络,点击登录,在响应头中可以看得cookie的相关数据

Set-Cookie: username=zhangsan; Expires=Sat, 27-Aug-2022 14:35:56 GMT
Set-Cookie: password=123; Expires=Sat, 27-Aug-2022 14:35:56 GMT

记住我-获取Cookie

  1. 需求
    登录成功并勾选了Remeber后,后端返回给前端的Cookie数据就已经存储好了,接下来就需要在页面获取Cookie中的数据,并把数据设置到登录页面的用户名和密码框中。
    如何在页面直接获取Cookie中的值呢?

  2. 实现流程分析
    在页面可以使用EL表达式,${cookie.key.value}
    key:指的是存储在cookie中的键名称,例如${cookie.username.value}

  3. 具体实现

    • 在login.jsp用户名的表单输入框使用value值给表单元素添加默认值,value可以使用${cookie.username.value}
    • 在login.jsp密码的表单输入框使用value值给表单元素添加默认值,value可以使用${cookie.password.value}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <form action="/brand_demo/loginServlet" method="post">
    <h1>登录系统</h1>
    <div>${login_msg} ${register_msg}</div>
    用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
    密码:<input name="password" type="password" value="${cookie.password.value}"><br>
    记住账号:<input name="remember" type="checkbox" value="1"><br>
    <input value="登录" type="submit">
    <a href="/brand_demo/register.jsp">没有账号?</a>
    </form>
    • 访问测试,重新访问登录页面,就可以看得用户和密码已经被填充。

用户注册功能

  1. 需求

    • 注册功能:保存用户信息到数据库
    • 验证码功能
      • 展示验证码:展示验证码图片,并可以点击切换
      • 校验验证码:验证码填写不正确,则注册失败
  2. 实现流程分析

    • 前端通过表单发送请求和数据给Web层的RegisterServlet
    • 在RegisterServlet中接收请求和数据[用户名和密码]
    • RegisterServlet接收到请求和数据后,调用Service层完成用户信息的保存
    • 在Service层需要编写UserService类,在类中实现register方法,需要判断用户是否已经存在,如果不存在,则完成用户数据的保存
    • 在UserMapper接口中,声明两个方法,一个是根据用户名查询用户信息方法,另一个是保存用户信息方法
    • 在UserService类中保存成功则返回true,失败则返回false,将数据返回给Web层
    • Web层获取到结果后,如果返回的是true,则提示注册成功,并转发到登录页面,如果返回false则提示用户名已存在并转发到注册页面
  3. 具体实现

    • 在UserService中实现注册逻辑
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //返回值告诉我们是否注册成功了
    public boolean register(User user) {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User u = mapper.selectByUsername(user.getUsername());
    if (u == null) {
    //u为null,说明未找到username对应的用户,即用户名不重复,可以注册
    mapper.add(user);
    //注册操作记得提交事务
    sqlSession.commit();
    sqlSession.close();
    }
    sqlSession.close();
    return u == null;
    }
    • 编写一个注册的页面register.jsp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@ page isELIgnored="false" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <form action="/brand_demo/registerServlet" method="post">
    <h1>欢迎注册</h1>
    <!--已有账号的话就跳转至登录页面-->
    已有账号?<a href="login.jsp">点击登录</a><br>
    用户名:<input name="username" type="text"><br>
    密码:<input name="password" type="password"><br>
    <input value="注册" type="submit">
    </form>
    </body>
    </html>
    • 编写RegisterServlet
    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
    @WebServlet("/registerServlet")
    public class RegisterServlet extends HttpServlet {
    private UserService userService = new UserService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //获取客户输入的用户名和密码
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    //封装成一个User
    User user = new User();
    user.setUsername(username);
    user.setPassword(password);
    //注册账号,用flag来接收是否注册成功
    boolean flag = userService.register(user);
    if (flag){
    //如果成功注册,将注册成功的提示存入request域中,随后跳转到登录页面
    request.setAttribute("register_msg","注册成功请登录");
    request.getRequestDispatcher("/login.jsp").forward(request,response);
    }else {
    //如果注册失败,就是用户名重复了,将错误信息存入request域中,并返回注册页面
    request.setAttribute("register_msg","用户名已存在");
    request.getRequestDispatcher("/register.jsp").forward(request,response);
    }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
    • 注册成功,跳转至登录页面,我们需要将注册成功的提示显示在登录页面上,修改login.jsp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <form action="/brand_demo/loginServlet" method="post">
    <h1>登录系统</h1>
    <!--还是放在这里,一个登录的错误提示信息,一个注册成功的提示信息,谁有信息就显示谁-->
    <div>${login_msg} ${register_msg}</div>
    用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
    密码:<input name="password" type="password" value="${cookie.password.value}"><br>
    记住账号:<input name="remember" type="checkbox" value="1"><br>
    <input value="登录" type="submit">
    <a href="/brand_demo/register.jsp">没有账号?</a>
    </form>
    • 注册失败,跳转至注册页面,同时将错误信息展示在注册页面上,修改register.jsp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <form action="/brand_demo/registerServlet" method="post">
    <h1>欢迎注册</h1>
    已有账号?<a href="login.jsp">点击登录</a><br>
    用户名:<input name="username" type="text"><br>
    <!--在这里显示一下注册失败的错误信息-->
    <div>${register_msg}</div>
    密码:<input name="password" type="password"><br>
    <input value="注册" type="submit">
    </form>
    • 启动测试,如果是注册的用户信息已经存在,则会显示用户名已存在,如果注册成功,会跳转至登录页面,同时也会显示注册成功请登录提示语句

验证码-展示

  1. 需求分析
    • 展示验证码:展示验证码图片,并可以点击看不清切换,验证码的生成是通过工具类来实现的
    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
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    package com.blog.util;

    import javax.imageio.ImageIO;
    import java.awt.*;
    import java.awt.geom.AffineTransform;
    import java.awt.image.BufferedImage;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.util.Arrays;
    import java.util.Random;

    /**
    * 生成验证码工具类
    */
    public class CheckCodeUtil {

    public static final String VERIFY_CODES = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static Random random = new Random();

    /**
    * 输出随机验证码图片流,并返回验证码值(一般传入输出流,响应response页面端,Web项目用的较多)
    *
    * @param w
    * @param h
    * @param os
    * @param verifySize
    * @return
    * @throws IOException
    */
    public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
    String verifyCode = generateVerifyCode(verifySize);
    outputImage(w, h, os, verifyCode);
    return verifyCode;
    }

    /**
    * 使用系统默认字符源生成验证码
    *
    * @param verifySize 验证码长度
    * @return
    */
    public static String generateVerifyCode(int verifySize) {
    return generateVerifyCode(verifySize, VERIFY_CODES);
    }

    /**
    * 使用指定源生成验证码
    *
    * @param verifySize 验证码长度
    * @param sources 验证码字符源
    * @return
    */
    public static String generateVerifyCode(int verifySize, String sources) {
    // 未设定展示源的字码,赋默认值大写字母+数字
    if (sources == null || sources.length() == 0) {
    sources = VERIFY_CODES;
    }
    int codesLen = sources.length();
    Random rand = new Random(System.currentTimeMillis());
    StringBuilder verifyCode = new StringBuilder(verifySize);
    for (int i = 0; i < verifySize; i++) {
    verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
    }
    return verifyCode.toString();
    }

    /**
    * 生成随机验证码文件,并返回验证码值 (生成图片形式,用的较少)
    *
    * @param w
    * @param h
    * @param outputFile
    * @param verifySize
    * @return
    * @throws IOException
    */
    public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
    String verifyCode = generateVerifyCode(verifySize);
    outputImage(w, h, outputFile, verifyCode);
    return verifyCode;
    }

    /**
    * 生成指定验证码图像文件
    *
    * @param w
    * @param h
    * @param outputFile
    * @param code
    * @throws IOException
    */
    public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
    if (outputFile == null) {
    return;
    }
    File dir = outputFile.getParentFile();
    //文件不存在
    if (!dir.exists()) {
    //创建
    dir.mkdirs();
    }
    try {
    outputFile.createNewFile();
    FileOutputStream fos = new FileOutputStream(outputFile);
    outputImage(w, h, fos, code);
    fos.close();
    } catch (IOException e) {
    throw e;
    }
    }

    /**
    * 输出指定验证码图片流
    *
    * @param w
    * @param h
    * @param os
    * @param code
    * @throws IOException
    */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
    int verifySize = code.length();
    BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
    Random rand = new Random();
    Graphics2D g2 = image.createGraphics();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    // 创建颜色集合,使用java.awt包下的类
    Color[] colors = new Color[5];
    Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
    Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
    Color.PINK, Color.YELLOW};
    float[] fractions = new float[colors.length];
    for (int i = 0; i < colors.length; i++) {
    colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
    fractions[i] = rand.nextFloat();
    }
    Arrays.sort(fractions);
    // 设置边框色
    g2.setColor(Color.GRAY);
    g2.fillRect(0, 0, w, h);

    Color c = getRandColor(200, 250);
    // 设置背景色
    g2.setColor(c);
    g2.fillRect(0, 2, w, h - 4);

    // 绘制干扰线
    Random random = new Random();
    // 设置线条的颜色
    g2.setColor(getRandColor(160, 200));
    for (int i = 0; i < 20; i++) {
    int x = random.nextInt(w - 1);
    int y = random.nextInt(h - 1);
    int xl = random.nextInt(6) + 1;
    int yl = random.nextInt(12) + 1;
    g2.drawLine(x, y, x + xl + 40, y + yl + 20);
    }

    // 添加噪点
    // 噪声率
    float yawpRate = 0.05f;
    int area = (int) (yawpRate * w * h);
    for (int i = 0; i < area; i++) {
    int x = random.nextInt(w);
    int y = random.nextInt(h);
    // 获取随机颜色
    int rgb = getRandomIntColor();
    image.setRGB(x, y, rgb);
    }
    // 添加图片扭曲
    shear(g2, w, h, c);

    g2.setColor(getRandColor(100, 160));
    int fontSize = h - 4;
    Font font = new Font("Algerian", Font.ITALIC, fontSize);
    g2.setFont(font);
    char[] chars = code.toCharArray();
    for (int i = 0; i < verifySize; i++) {
    AffineTransform affine = new AffineTransform();
    affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
    g2.setTransform(affine);
    g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
    }

    g2.dispose();
    ImageIO.write(image, "jpg", os);
    }

    /**
    * 随机颜色
    *
    * @param fc
    * @param bc
    * @return
    */
    private static Color getRandColor(int fc, int bc) {
    if (fc > 255) {
    fc = 255;
    }
    if (bc > 255) {
    bc = 255;
    }
    int r = fc + random.nextInt(bc - fc);
    int g = fc + random.nextInt(bc - fc);
    int b = fc + random.nextInt(bc - fc);
    return new Color(r, g, b);
    }

    private static int getRandomIntColor() {
    int[] rgb = getRandomRgb();
    int color = 0;
    for (int c : rgb) {
    color = color << 8;
    color = color | c;
    }
    return color;
    }

    private static int[] getRandomRgb() {
    int[] rgb = new int[3];
    for (int i = 0; i < 3; i++) {
    rgb[i] = random.nextInt(255);
    }
    return rgb;
    }

    private static void shear(Graphics g, int w1, int h1, Color color) {
    shearX(g, w1, h1, color);
    shearY(g, w1, h1, color);
    }

    private static void shearX(Graphics g, int w1, int h1, Color color) {

    int period = random.nextInt(2);

    boolean borderGap = true;
    int frames = 1;
    int phase = random.nextInt(2);

    for (int i = 0; i < h1; i++) {
    double d = (double) (period >> 1)
    * Math.sin((double) i / (double) period
    + (6.2831853071795862D * (double) phase)
    / (double) frames);
    g.copyArea(0, i, w1, 1, (int) d, 0);
    if (borderGap) {
    g.setColor(color);
    g.drawLine((int) d, i, 0, i);
    g.drawLine((int) d + w1, i, w1, i);
    }
    }

    }

    private static void shearY(Graphics g, int w1, int h1, Color color) {

    int period = random.nextInt(40) + 10; // 50;

    boolean borderGap = true;
    int frames = 20;
    int phase = 7;
    for (int i = 0; i < w1; i++) {
    double d = (double) (period >> 1)
    * Math.sin((double) i / (double) period
    + (6.2831853071795862D * (double) phase)
    / (double) frames);
    g.copyArea(i, 0, 1, h1, 0, (int) d);
    if (borderGap) {
    g.setColor(color);
    g.drawLine(i, (int) d, i, 0);
    g.drawLine(i, (int) d + h1, i, h1);
    }

    }

    }
    }
    • 验证码就是使用Java代码生成的一张图片
    • 验证码的作用:防止机器自动注册,攻击服务器
  2. 实现流程分析
    • 前端发送请求给CheckCodeServlet
    • CheckCodeServlet接收到请求后,生成验证码图片,将图片用Reponse对象的输出流写回到前端
    • 思考:如何将图片写回到前端浏览器呢?
      • 前面在学Reponse对象的时候,它有一个方法可以获取其字节输出流,getOutputStream()
      • 我们可以把写往磁盘的流对象更好成Response的字节流,即可完成图片响应给前端
  3. 具体实现
    • 修改Register.jsp页面,将验证码的图片从后台获取
      看不清绑定一个点击事件,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <form action="/brand_demo/registerServlet" method="post">
    <h1>欢迎注册</h1>
    已有账号?<a href="login.jsp">点击登录</a><br>
    用户名:<input name="username" type="text"><br>
    <div>${register_msg}</div>
    密码:<input name="password" type="password"><br>
    验证码:<input name="checkCode" type="text">
    <img id="checkCodeImg" src="/brand_demo/checkCodeServlet">
    <a href="#" id="checkImg">看不清?</a>
    <input value="注册" type="submit">
    </form>

    <script>
    document.getElementById("checkImg").onclick = function () {
    //路径后面加一个时间戳,能保证生成的图片永远不一样,避免浏览器缓存静态资源
    document.getElementById("checkCodeImg").src = "/brand_demo/checkCodeServlet?" + new Date().getMilliseconds();
    }
    </script>
    • 编写CheckCodeServlet类,用来接收请求生成验证码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @WebServlet("/checkCodeServlet")
    public class CheckCodeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //生成验证码
    OutputStream os = response.getOutputStream();
    String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
    • 完成这些之后,我们启动服务器,就可以看到验证码了,点击看不清,也会自动刷新验证码

验证码-校验

  1. 需求

    • 判断程序生成的验证码 和 用户输入的验证码 是否一样,如果不一样,则阻止注册
    • 验证码图片访问和提交注册表单是两次请求,所以要将程序生成的验证码存入Session中
    • 思考:为什么要把验证码数据存入到Session中呢?
      • 生成验证码和校验验证码是两次请求,此处就需要在一个会话的两次请求之间共享数据
      • 验证码属于安全数据类的,所以我们选中Session来存储验证码数据。
  2. 实现流程分析

    • 在CheckCodeServlet中生成验证码的时候,将验证码数据存入Session对象
    • 前端将验证码和注册数据提交到后台,交给RegisterServlet类
    • RegisterServlet类接收到请求和数据后,其中就有验证码,和Session中的验证码进行对比
    • 如果一致,则完成注册,如果不一致,则提示错误信息
  3. 具体实现

    • 修改CheckCodeServlet类,将验证码存入Session域
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @WebServlet("/checkCodeServlet")
    public class CheckCodeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //生成验证码
    OutputStream os = response.getOutputStream();
    String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
    //将验证码存入session域
    HttpSession session = request.getSession();
    session.setAttribute("checkCodeGen", checkCode);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
    • 在RegisterServlet中,获取页面的和session对象中的验证码,进行对比
    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
    @WebServlet("/registerServlet")
    public class RegisterServlet extends HttpServlet {
    private UserService userService = new UserService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //获取客户输入的用户名和密码
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    //多获取一个用户输入的验证码
    String checkCode = request.getParameter("checkCode");
    //封装成一个User
    User user = new User();
    user.setUsername(username);
    user.setPassword(password);
    //从Session中获取验证码
    HttpSession session = request.getSession();
    String checkCodeGen = (String) session.getAttribute("checkCodeGen");
    //如果验证码不一致
    if (!checkCodeGen.equals(checkCode)) {
    //将错误信息存储到request域中
    request.setAttribute("checkCode_msg", "验证码输入错误");
    //同时跳转至注册页面
    request.getRequestDispatcher("/register.jsp").forward(request, response);
    //不允许注册,结束本次操作
    return;
    }
    //注册账号,用flag来接收是否注册成功
    boolean flag = userService.register(user);
    if (flag) {
    //如果成功注册,将注册成功的提示存入request域中,随后跳转到登录页面
    request.setAttribute("register_msg", "注册成功请登录");
    request.getRequestDispatcher("/login.jsp").forward(request, response);
    } else {
    //如果注册失败,就是用户名重复了,将错误信息存入request域中,并返回注册页面
    request.setAttribute("register_msg", "用户名已存在");
    request.getRequestDispatcher("/register.jsp").forward(request, response);
    }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
    • 验证码错误,需要在注册页面上显示提示信息,修改register.jsp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <form action="/brand_demo/registerServlet" method="post">
    <h1>欢迎注册</h1>
    已有账号?<a href="login.jsp">点击登录</a><br>
    用户名:<input name="username" type="text"><br>
    <div>${register_msg}</div>
    密码:<input name="password" type="password"><br>
    <div>${checkCode_msg}</div> <!--在这里显示一个提示信息-->
    验证码:<input name="checkCode" type="text">
    <img id="checkCodeImg" src="/brand_demo/checkCodeServlet">
    <a href="#" id="checkImg">看不清?</a>
    <input value="注册" type="submit">
    </form>
    • 至此,用户的注册登录功能就已经完成了。