day10 会话&书城项目第四阶段
1.会话
1.1 为什么需要会话控制
保持用户登录状态,就是当用户在登录之后,会在服务器中保存该用户的登录状态,当该用户后续访问该项目中的其它动态资源(Servlet或者Thymeleaf)的时候,能够判断当前是否是已经登录过的。而从用户登录到用户退出登录这个过程中所发生的所有请求,其实都是在一次会话范围之内
1.2 域对象的范围
1.2.1 应用域的范围
整个项目部署之后,只会有一个应用域对象,所有客户端都是共同访问同一个应用域对象,在该项目的所有动态资源中也是共用一个应用域对象
1.2.2 请求域的范围
每一次请求都有一个请求域对象,当请求结束的时候对应的请求域对象也就销毁了
1.2.3 会话域的范围
会话域是从客户端连接上服务器开始,一直到客户端关闭,这一整个过程中发生的所有请求都在同一个会话域中;而不同的客户端是不能共用会话域的
1.3 Cookie技术
1.3.1 Cookie的概念
Cookie是一种客户端的会话技术,它是服务器存放在浏览器的一小份数据,浏览器以后每次访问该服务器的时候都会将这小份数据携带到服务器去。
1.3.2 Cookie的作用
- 在浏览器中存放数据
- 将浏览器中存放的数据携带到服务器
1.3.3 Cookie的应用场景
1.记住用户名
当我们在用户名的输入框中输入完用户名后,浏览器记录用户名,下一次再访问登录页面时,用户名自动填充到用户名的输入框.
2.保存电影的播放进度
在网页上播放电影的时候,如果中途退出浏览器了,下载再打开浏览器播放同一部电影的时候,会自动跳转到上次退出时候的进度,因为在播放的时候会将播放进度保存到cookie中
1.3.4 Cookie的入门案例
① 目标
实现在ServletDemo01和ServletDemo02之间共享数据,要求在会话域范围内共享
② Cookie相关的API
- 创建一个Cookie对象(cookie只能保存字符串数据。且不能保存中文)
new Cookie(String name,String value);
- 把cookie写回浏览器
response.addCookie(cookie);
- 获得浏览器带过来的所有Cookie:
request.getCookies() ; //得到所有的cookie对象。是一个数组,开发中根据key得到目标cookie
- cookie的 API
cookie.getName() ; //返回cookie中设置的key
cookie.getValue(); //返回cookie中设置的value
③ ServletDemo01代码
在ServletDemo01中创建Cookie数据并响应给客户端
public class ServletDemo01 extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 创建一个cookie对象,用于存放键值对
Cookie cookie = new Cookie("cookie-message","hello-cookie");
//2. 将cookie添加到response中
//底层是通过一个名为"Set-Cookie"的响应头携带到浏览器的
response.addCookie(cookie);
}
}
④ 浏览器发送请求携带Cookie
这里不需要我们操作,浏览器会在给服务器发送请求的时候,将cookie通过请求头自动携带到服务器
⑤ ServletDemo02获取Cookie数据的代码
public class ServletDemo02 extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 从请求中取出cookie
//底层是由名为"Cookie"的请求头携带的
Cookie[] cookies = request.getCookies();
//2. 遍历出每一个cookie
if (cookies != null) {
for (Cookie cookie : cookies) {
//匹配cookie的name
if (cookie.getName().equals("cookie-message")) {
//它就是我们想要的那个cookie
//我们就获取它的value
String value = cookie.getValue();
System.out.println("在ServletDemo02中获取str的值为:" + value);
}
}
}
}
}
1.3.5 Cookie的时效性
如果我们不设置Cookie的时效性,默认情况下Cookie的有效期是一次会话范围内,我们可以通过cookie的setMaxAge()方法让Cookie持久化保存到浏览器上
- 会话级Cookie
- 服务器端并没有明确指定Cookie的存在时间
- 在浏览器端,Cookie数据存在于内存中
- 只要浏览器还开着,Cookie数据就一直都在
- 浏览器关闭,内存中的Cookie数据就会被释放
- 持久化Cookie
- 服务器端明确设置了Cookie的存在时间
- 在浏览器端,Cookie数据会被保存到硬盘上
- Cookie在硬盘上存在的时间根据服务器端限定的时间来管控,不受浏览器关闭的影响
- 持久化Cookie到达了预设的时间会被释放
cookie.setMaxAge(int expiry)
参数单位是秒,表示cookie的持久化时间,如果设置参数为0,表示将浏览器中保存的该cookie删除
1.3.6 Cookie的path
上网时间长了,本地会保存很多Cookie。对浏览器来说,访问互联网资源时不能每次都把所有Cookie带上。浏览器会使用Cookie的path属性值来和当前访问的地址进行比较,从而决定是否携带这个Cookie。
我们可以通过调用cookie的setPath()方法来设置cookie的path
Cookie c3 = new Cookie("hero", encode);
Cookie c4 = new Cookie("title1", "sanguo");
// /day37/c3 只有这个路径下的请求才会携带cookie 其他路径访问不会携带此cookie
c4.setPath(req.getContextPath() + "/c3");
Cookie c5 = new Cookie("title2", "shuihu");
c5.setPath(req.getContextPath() + "/c3");
1.4 Session技术
1.4.1 session概述
session是服务器端的技术。服务器为每一个浏览器开辟一块内存空间,即session对象。由于session对象是每一个浏览器特有的,所以用户的记录可以存放在session对象中
1.4.2 Session的入门案例
① 目标
实现在SessionServlet1和SessionServlet2之间共享数据,要求在会话域范围内共享
② Session的API介绍
- request.getSession(); 获得session(如果第一次调用的时候其实是创建session,第一次之后通过sessionId找到session进行使用)
- Object getAttribute(String name) ;获取值
- void setAttribute(String name, Object value) ;存储值
- void invalidate(); 设置session失效
- void removeAttribute(String name) ;移除指定name的值
- void setMaxInactiveInterval(int var1); 设置不活跃时间 单位是 秒
③ 在SessionServlet1中往Session域对象存储数据
package com.atguigu.servlet.session;
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 java.io.IOException;
@WebServlet("/s1")
public class SessionServlet1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 解决乱码问题
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
System.out.println("------SessionServlet1 成功访问-------");
// tomcat创建Session对象
HttpSession session = req.getSession();
String id = session.getId();
// 向会话域添加了一个数据
session.setAttribute("username", "李白");
session.setAttribute("pwd", "123456");
System.out.println("session = " + session);
resp.getWriter().println("SessionServlet1 成功访问..." + id);
}
}
④ 在SessionServlet2中从Session域对象中获取数据
package com.atguigu.servlet.session;
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 java.io.IOException;
@WebServlet("/s2")
public class SessionServlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
System.out.println("------SessionServlet2 成功访问-----------");
HttpSession session = req.getSession();
String id = session.getId();
// 根据name获取会话域中的数据
Object username = session.getAttribute("username");
System.out.println("username = " + username);
Object pwd = session.getAttribute("pwd");
System.out.println("pwd = " + pwd);
resp.getWriter().println("SessionServlet2 成功访问" + id);
}
}
1.4.3 Session的工作机制
前提:浏览器正常访问服务器
- 服务器端没调用request.getSession()方法:什么都不会发生
- 服务器端调用了request.getSession()方法
- 服务器端检查当前请求中是否携带了JSESSIONID的Cookie
- 有:根据JSESSIONID在服务器端查找对应的HttpSession对象
- 能找到:将找到的HttpSession对象作为request.getSession()方法的返回值返回
- 找不到:服务器端新建一个HttpSession对象作为request.getSession()方法的返回值返回
- 无:服务器端新建一个HttpSession对象作为request.getSession()方法的返回值返回
- 有:根据JSESSIONID在服务器端查找对应的HttpSession对象
- 服务器端检查当前请求中是否携带了JSESSIONID的Cookie
代码验证
// 1.调用request对象的方法尝试获取HttpSession对象
HttpSession session = request.getSession();
// 2.调用HttpSession对象的isNew()方法
boolean wetherNew = session.isNew();
// 3.打印HttpSession对象是否为新对象
System.out.println("wetherNew = " + wetherNew+"HttpSession对象是新的":"HttpSession对象是旧的"));
// 4.调用HttpSession对象的getId()方法
String id = session.getId();
// 5.打印JSESSIONID的值
System.out.println("JSESSIONID = " + id);
1.4.4 Session的时效性
① 为什么Session要设置时限
用户量很大之后,Session对象相应的也要创建很多。如果一味创建不释放,那么服务器端的内存迟早要被耗尽。
② 设置时限的难点
从服务器端的角度,很难精确得知类似浏览器关闭的动作。而且即使浏览器一直没有关闭,也不代表用户仍然在使用。
③ 服务器端给Session对象设置最大闲置时间
- 默认值:1800秒
最大闲置时间生效的机制如下:
④ 代码验证
package com.atguigu.servlet.session;
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 java.io.IOException;
@WebServlet("/s5")
public class SessionServlet5 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
System.out.println("------------SessionServlet5 成功访问---------");
// 指定在servlet容器使此会话失效之前的时间间隔,以秒为单位
HttpSession session = req.getSession();
// 设置session失效 20秒内没请求session将会失效 20s内有请求 将向后顺延20s
session.setMaxInactiveInterval(20);
resp.getWriter().println("SessionServlet5 成功访问");
}
}
⑤ 强制Session立即失效
package com.atguigu.servlet.session;
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 java.io.IOException;
@WebServlet("/s3")
public class SessionServlet3 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
System.out.println("------------SessionServlet3 成功访问---------");
HttpSession session = req.getSession();
// 设置session失效
session.invalidate();
resp.getWriter().println("SessionServlet3 成功访问");
}
}
⑥移除指定name的值
package com.atguigu.servlet.session;
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 java.io.IOException;
@WebServlet("/s4")
public class SessionServlet4 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
System.out.println("-------SessionServlet4 成功访问--------");
HttpSession session = req.getSession();
// 移除指定name的值
session.removeAttribute("pwd");
resp.getWriter().println("SessionServlet4 成功访问");
}
}
2. 书城项目第四阶段
2.1 保持登录状态
2.1.1 迁移项目
创建一个新Module,将旧Module中的内容迁移
- 迁移src目录下的Java代码
- 迁移web目录下的static目录
- 迁移web/WEB-INF目录下的lib目录和pages目录
- 将lib目录下的jar包添加到运行时环境
- 将旧的web.xml中的配置复制到新module的web.xml中
2.1.2 将登录成功的User存入Session中
// 登录逻辑
protected void login(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取请求参数
Map<String, String[]> map = req.getParameterMap();
// map.forEach((k,v) -> System.out.println(k + "," + Arrays.toString(v)));
// 获取check的值 判断是否勾选三天内免登录
String check = req.getParameter("check");
System.out.println("check = " + check);
// 将请求参数封装到User对象中
User user = new User();
try {
BeanUtils.populate(user, map);
// 调用业务层的方法处理登录
service.doLogin(user);
Cookie username = new Cookie("username", user.getUsername());
Cookie password = new Cookie("password", user.getPassword());
if (check != null){
// 如果勾选三天内免登录 设置cookie时间为3天
username.setMaxAge(60 * 60 * 24 * 3);
password.setMaxAge(60 * 60 * 24 * 3);
resp.addCookie(username);
resp.addCookie(password);
}else{
// 否则cookie时间为当此会话
resp.addCookie(username);
resp.addCookie(password);
}
// 调用toLoginSuccessPage方法
resp.sendRedirect(req.getContextPath() + "/user?method=toLoginSuccessPage");
} catch (Exception e) {
e.printStackTrace();
// 出现异常 返回异常信息
req.setAttribute("eMsg", e.getMessage());
System.out.println("验证码不正确");
// 跳转到登录页面
processTemplate("user/login", req, resp);
}
}
2.1.3 修改欢迎信息
① 登录成功页面
<span>欢迎<span class="um_span" th:text="${username}">张总</span>光临尚硅谷书城</span>
② 首页
<div class="topbar-right" th:if="${username} == null">
<a href="user?method=toLoginPage" class="login">登录</a>
<a href="user?method=toRegistPage" class="register">注册</a>
<a
href="pages/cart/cart.html"
class="cart iconfont icon-gouwuche
"
>
购物车
<div class="cart-num">3</div>
</a>
<a th:href="admin" class="admin">后台管理</a>
</div>
<div class="topbar-right" th:unless="${username} == null">
<span>欢迎你 <b th:text="${username}">张总</b> </span>
<a th:href="@{/user(method=logout)}">注销</a>
<a
href="pages/cart/cart.html"
class="cart iconfont icon-gouwuche
"
>
购物车
<div class="cart-num">3</div>
</a>
<a th:href="admin" class="admin">后台管理</a>
</div>
2.2 退出登录功能
2.2.1 目标
用户退出登录的时候,清除会话域中保存的当前用户的所有信息
2.2.2 页面超链接
<a th:href="@{/user(method=logout)}">注销</a>
2.2.3 UserServlet.logout()
// 注销用户
protected void logout(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String name = cookie.getName();
if ("username".equals(name)){
// cookie时间设为0 注销用户
cookie.setMaxAge(0);
}
}
}
// 跳转到登录页面
resp.sendRedirect(req.getContextPath() + "/user?method=toLoginPage");
}
2.3 验证码
2.3.1 目标
通过让用户填写验证码并在服务器端检查,防止浏览器端使用程序恶意访问。
2.3.2 思路
2.3.3 操作
① 导入jar包
kaptcha-2.3.2.jar
② 配置KaptchaServlet
jar包中已经写好了Servlet的Java类,我们只需要在web.xml中配置这个Servlet即可。
<servlet>
<servlet-name>KaptchaServlet</servlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
<!-- 设置验证码的内容-->
<init-param>
<param-name>kaptcha.textproducer.char.string</param-name>
<param-value>0123456789</param-value>
</init-param>
<!-- 设置验证码的长度-->
<init-param>
<param-name>kaptcha.textproducer.char.length</param-name>
<param-value>4</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>KaptchaServlet</servlet-name>
<url-pattern>/KaptchaServlet</url-pattern>
</servlet-mapping>
③ 通过页面访问测试
http://localhost:8080/bookstore/KaptchaServlet
④ 在注册页面显示验证码图片
<img src="KaptchaServlet" alt="" />
⑤ 调整验证码图片的显示效果
a. 去掉边框
KaptchaServlet会在初始化时读取init-param,而它能够识别的init-param在下面类中:
com.google.code.kaptcha.util.Config
web.xml中具体配置如下:
<servlet>
<servlet-name>KaptchaServlet</servlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
<!-- 通过配置初始化参数影响KaptchaServlet的工作方式 -->
<!-- 可以使用的配置项参考com.google.code.kaptcha.util.Config类 -->
<!-- 配置kaptcha.border的值为no取消图片边框 -->
<init-param>
<param-name>kaptcha.border</param-name>
<param-value>no</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>KaptchaServlet</servlet-name>
<url-pattern>/KaptchaServlet</url-pattern>
</servlet-mapping>
开发过程中的工程化细节:
no、false、none等等单词从含义上来说都表示『没有边框』这个意思,但是这里必须使用no。
参考的依据是下面的源码:
public boolean getBoolean(String paramName, String paramValue, boolean defaultValue) {
boolean booleanValue;
if (!"yes".equals(paramValue) && !"".equals(paramValue) && paramValue != null) {
if (!"no".equals(paramValue)) {
throw new ConfigException(paramName, paramValue, "Value must be either yes or no.");
}
booleanValue = false;
} else {
booleanValue = defaultValue;
}
return booleanValue;
}
b. 设置图片大小
<img style="width: 150px; height: 40px;" src="KaptchaServlet" alt="" />
⑥ 点击图片刷新
a. 目的
验证码图片都是经过刻意扭曲、添加了干扰、角度偏转,故意增加了识别的难度。所以必须允许用户在看不出来的时候点击图片刷新,生成新的图片重新辨认。
b. 实现的代码
修改图片的img标签:
<img :src="imgPath" alt="" height="45px" width="140" @click="changeImg" />
Vue代码:定义数据模型
"data":{
...
// 验证码信息
"code":"",
"imgPath":"KaptchaServlet",
"codeErrMsg":"验证码为4~6位数字和字母组成",
"codeCss":{
"visibility":"hidden"
}
}
Vue代码:定义刷新验证码的函数
//切换验证码,其实就是重新设置img标签的src
"changeImg":function (){
console.log(111);
//Math.random();目的是让data中 imgPath的值每次点击都不一样 这样才会重新赋值 才会发起新的请求
this.imgPath = "KaptchaServlet?a=" + Math.random();
},
⑦ 执行注册前检查验证码
a. 确认KaptchaServlet将验证码存入Session域时使用的属性名
通过查看源码,找到验证码存入Session域时使用的属性名是:
KAPTCHA_SESSION_KEY
在执行注册的方法中添加新的代码
// 注册逻辑
protected void regist(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取所有的请求参数
Map<String, String[]> parameterMap = req.getParameterMap();
// 将parameterMap中的数据封装到User对象中
User user = new User();
try {
// 获取用户输入的验证码
String code = parameterMap.get("code")[0];
// 从session中获取服务器生产的验证码
String checkCode = (String)req.getSession().getAttribute("KAPTCHA_SESSION_KEY");
// 校验验证码 忽略大小写
if (checkCode.equalsIgnoreCase(code)){
// 验证码正确 才进行注册
BeanUtils.populate(user, parameterMap);
// 创建业务层的对象
// 调用业务层的方法 如果注册失败了 业务层会抛出异常
service.doRegister(user);
// 创建cookie 值为用户名
Cookie username = new Cookie("username", parameterMap.get("username")[0]);
resp.addCookie(username);
// 调用toRegistSuccessPage方法
resp.sendRedirect(req.getContextPath() + "/user?method=toRegistSuccessPage");
}else{
// 验证码错误
throw new RuntimeException("验证码错误,请重新输入");
}
} catch (Exception e) {
e.printStackTrace();
// 如果出现异常 那就是注册失败
req.setAttribute("errMsg", e.getMessage());
// 跳转到注册页面
processTemplate("user/regist", req, resp);
}
}