Cookie
会话可简单理解为:用户打开一个浏览器,点击多个超链接访问服务器多个Web资源,然后关闭浏览器的过程。
有状态会话:一个同学来过教室,下次再来教室,我们会知道这个同学曾经来过,这称之为有状态会话。
每个用户在使用浏览器与服务器进行会话过程中,不可避免各自产生一些数据,程序要想办法为每个用户保存这些数据。
基础入门
保存会话的技术有两种,它们分别如下:Cookie和Session
Cookie是客户端技术,程序把每个用户的数据以cookie形式写给用户各自的浏览器,当用户使用浏览器去访问服务中的Web资源时,就会携带各自的数据过去。这样Web资源处理的就是各个用户的数据了。
Session是服务端技术,服务器在运行时可以为每个用户的浏览器创建一个独享的Session对象,由于Session未用户浏览器独享,所以用户在访问服务器的Web资源时,可以把各自的数据放在各自的Session中,当用户再去访问服务器中的其他Web资源时,其他Web资源再从各自的Session对象中取出数据来为用户服务。
Cookie常用API
Java中的javax.servlet.http.Cookie
类用于创建一个Cookie
方法 | 描述 |
---|---|
Cookie(String name, String value) | 实例化Cookie对象,传入cooke名称和cookie的值。 |
public String getName() | 取得Cookie的名字 |
public String getValue() | 取得Cookie的值 |
public void setValue(String newValue) | 设置Cookie的值 |
public void setMaxAge(int expiry) | 设置Cookie的最大保存时间 |
public int getMaxAge() | 获取Cookies的有效期 |
public void setPath(String uri) | 设置cookie的有效路径 |
public String getPath() | 获取cookie的有效路径 |
public void setDomain(String pattern) | 设置cookie的有效域 |
public String getDomain() | 获取cookie的有效域 |
request接口中定义了一个getCookies()方法,它用于获取客户端提交的Cookie。
response接口也中定义了一个addCookie()方法,它用于在其响应过程增加一个相应的Set-Cookie头字段。
范例:使用cookie记录上一次访问时间
public class RequestDemo3 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 解决中文乱码问题
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
Cookie[] cookies = request.getCookies();
// cookies为空表示是第一次访问
if (cookies != null) {
writer.print("您上一次访问网页的时间是:");
for (int i = 0; i < cookies.length; i++) {
Cookie cookie = cookies[i];
if ("lastAccessTime".equals(cookie.getName())) {
long parseLong = Long.parseLong(cookie.getValue());
Date date = new Date(parseLong);
writer.write(date.toString());
}
}
}else {
writer.write("这是您第一次访问网页!");
}
//用户访问过之后重新设置用户的访问时间,存储到cookie中,然后发送到客户端浏览器
Cookie cookie = new Cookie("lastAccessTime", String.valueOf(System.currentTimeMillis()));
response.addCookie(cookie);
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
首先我们将浏览器的cookies都清理掉,然后访问该servlet会得到如下结果:
点击浏览器的刷新按钮,进行第二次访问,此时就服务器就可以通过cookie获取浏览器上一次访问的时间了,效果如下:
上面的实例中没有使用setMaxAge()设置cookie的有效期,cookie在浏览器关闭后就失效,如果想让cookie依然有效,必须设置有效期。
//用户访问过之后重新设置用户的访问时间,存储到cookie中,然后发送到客户端浏览器
Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");
//设置Cookie的有效期为1天
cookie.setMaxAge(24 * 60 * 60);
//将cookie对象添加到response对象中,这样服务器在输出response对象中的内容时就会把cookie也输出到客户端浏览器
response.addCookie(cookie);
这样即使关闭了浏览器,下次再访问时,也依然可以通过cookie获取用户上一次访问的时间。
Cookie注意细节
Cookie包含如下需要注意的点:
一个Cookie只能标识一种信息,它至少包含有一个标识该信息的名称(NAME)和值(VALUE)。
一个Web站点可以给一个浏览器发送多个Cookie,浏览器也可以存储多个Web站点提供的Cookie。
浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie大小限制为4kb。
在没有设置Cookie有效期的情况下,当浏览器退出时间,Cookie会被删除。如果想Cookie存储在磁盘上,则需要设置有效期。
删除Cookie
在删除Cookie时,必须保持path一致,否则无法删除:
public class CookieDemo1 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//创建一个名字为lastAccessTime的cookie
Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");
//将cookie的有效期设置为0,命令浏览器删除该cookie
cookie.setMaxAge(0);
response.addCookie(cookie);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
Cookie中存取中文
要想在cookie中存储中文,那么必须使用URLEncoder类里面的encode(String s, String enc)方法进行中文转码,例如:
Cookie cookie = new Cookie("userName", URLEncoder.encode("小明", "UTF-8"));
response.addCookie(cookie);
在获取cookie中的中文数据时,再使用URLDecoder类里面的decode(String s, String enc)进行解码,例如:
URLDecoder.decode(cookies[i].getValue(), "UTF-8")
Session
在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,
在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的
session中取出该用户的数据,为用户服务。
Session和Cookie的主要区别如下:
Cookie是把用户的数据写给用户的浏览器。
Session技术把用户的数据写到用户独占的session中。
Session对象由服务器创建,开发人员可以调用request对象的getSession方法得到session对象。
Session使用
Session实现原理
服务器创建Session出来后,会将session的id号以cookie形式回馈给浏览器,只要浏览器在不关闭的情况下再次访问服务器时,都会携带session的id号过去。
服务器发现浏览器携带过来的session的id后,会通过id来指定内存中与之对应的session为之服务。
public class SessionDemo1 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setCharacterEncoding("UTF=8");
response.setContentType("text/html;charset=UTF-8");
//使用request对象的getSession()获取session,如果session不存在则创建一个
HttpSession session = request.getSession();
//将数据存储到session中
session.setAttribute("data", "小明");
//获取session的Id
String sessionId = session.getId();
//判断session是不是新创建的
if (session.isNew()) {
response.getWriter().print("session创建成功,session的id是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在该session了,session的id是:"+sessionId);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
第一次访问时,服务器会创建一个新的sesion,并且把session的Id以cookie的形式发送给客户端浏览器,如下图所示:
点击刷新按钮,再次请求服务器,此时就可以看到浏览器再请求服务器时,会把存储到cookie中的session的Id一起传递到服务器端:
request.getSession()方法内部新创建了Session之后一定是做了如下的处理:
//获取session的Id
String sessionId = session.getId();
//将session的Id存储到名字为JSESSIONID的cookie中
Cookie cookie = new Cookie("JSESSIONID", sessionId);
//设置cookie的有效路径
cookie.setPath(request.getContextPath());
response.addCookie(cookie);
浏览器禁用Cookie处理
IE8禁用cookie
工具->internet选项->隐私->设置->将滑轴拉到最顶上(阻止所有cookies)
解决方案:
response.encodeRedirectURL(java.lang.String url) 用于对sendRedirect方法后的url地址进行重写。
response.encodeURL(java.lang.String url)用于对表单action和超链接的url地址进行重写
使用Servlet共享Session中的数据来解决浏览器禁用Cookie的问题:
IndexServlet:
// 列出所有图书
public class IndexServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
HttpSession session = request.getSession();
writer.write("本网站有如下图书:<br/>");
Set<Entry<String,Book>> entrySet = DBUtils.getAlll().entrySet();
for (Entry<String, Book> entry : entrySet) {
Book book = entry.getValue();
String url = request.getContextPath()+ "/servlet/BuyServlet?id=" + book.getId();
// 将超链接的url地址进行重写
url = response.encodeUrl(url);
writer.write(book.getName() + " <a href='" + url + "'>购买</a><br/>");
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
DBUtils类和Book类:
// 模拟数据库
public class DBUtils {
private static Map<String, Book> map = new LinkedHashMap<String, Book>();
static{
map.put("1", new Book("1", "javaweb开发"));
map.put("2", new Book("2", "spring开发"));
map.put("3", new Book("3", "hibernate开发"));
map.put("4", new Book("4", "structs开发"));
map.put("5", new Book("5", "ajax开发"));
}
public static Map<String, Book> getAlll(){
return map;
}
}
// Book类
public class Book {
private String id;
private String name;
public Book(String id, String name) {
super();
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
ListCartServlet:
public class ListCartServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
HttpSession session = request.getSession();
List<Book> list = (List<Book>) session.getAttribute("list");
if (null == list || list.size() <= 0) {
writer.write("对不起,您还没有购买任何商品!!");
return;
}
// 显示用户购买过的商品
writer.write("您购买过如下商品:<br/>");
for (Book book : list) {
writer.write(book.getName() + "<br/>");
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
BuyServlet:
public class BuyServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String id = request.getParameter("id");
// 得到用户想要购买的书籍
Book book = DBUtils.getAlll().get(id);
HttpSession session = request.getSession();
// 得到用户用于保存的所有书籍
List<Book> list = (List) session.getAttribute("list");
if (null == list || list.size() <= 0) {
list = new ArrayList<Book>();
session.setAttribute("list", list);
}
list.add(book);
//response. encodeRedirectURL(java.lang.String url)用于对sendRedirect方法后的url地址进行重写
String url = response.encodeRedirectUrl(request.getContextPath() + "/servlet/ListCartServlet");
System.out.println(url);
response.sendRedirect(url);
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
在禁用了cookie的IE8下的运行效果如下:
通过查看IndexServlet生成的html代码可以看到,每一个超链接后面都带上了session的Id,如下所示
本网站有如下书:<br/>
javaweb开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=1'>购买</a><br/>
spring开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=2'>购买</a><br/>
hibernate开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=3'>购买</a><br/>
struts开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=4'>购买</a><br/>
ajax开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=5'>购买</a><br/>
所以,当浏览器禁用了cookie后,就可以用URL重写这种解决方案解决Session数据共享问题。而且response. encodeRedirectURL(java.lang.String url) 和response. encodeURL(java.lang.String url)是两个非常智能的方法,当检测到浏览器没有禁用cookie时,那么就不进行URL重写了。我们在没有禁用cookie的火狐浏览器下访问,效果如下:
![img](file:///C:/Users/Legend/Documents/My Knowledge/temp/e0f7d0ba-0575-49e7-b762-3f06a62a310e/128/index_files/201833071467404.gif)
从演示动画中可以看到,浏览器第一次访问时,服务器创建Session,然后将Session的Id以Cookie的形式发送回给浏览器,response. encodeURL(java.lang.String url)
方法也将URL进行了重写,当点击刷新按钮第二次访问,由于火狐浏览器没有禁用cookie,所以第二次访问时带上了cookie,此时服务器就可以知道当前的客户端浏览器
并没有禁用cookie,那么就通知response. encodeURL(java.lang.String url)方法不用将URL进行重写了。
Session创建和销毁时机
Session对象的创建时机
在程序中第一次调用request.getSession()方法时就会创建一个新的Session,可以用isNew()方法来判断Session是不是新创建的
//使用request对象的getSession()获取session,如果session不存在则创建一个
HttpSession session = request.getSession();
//获取session的Id
String sessionId = session.getId();
//判断session是不是新创建的
if (session.isNew()) {
response.getWriter().print("session创建成功,session的id是:" + sessionId);
}else {
response.getWriter().print("服务器已经存在session,session的id是:" + sessionId);
}
Session对象的销毁时机
session对象默认30分钟没有使用,则服务器会自动销毁session,在web.xml文件中可以手工配置session的失效时间,例如:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name></display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- 设置Session的有效时间:以分钟为单位-->
<session-config>
<session-timeout>15</session-timeout>
</session-config>
</web-app>
当需要在程序中手动设置Session失效时,可以手工调用session.invalidate方法,摧毁session。
HttpSession session = request.getSession();
//手工调用session.invalidate方法,摧毁session
session.invalidate();
Session防止表单重复提交
表单重复提交场景
在开发中,如果网速比较慢的情况下,用户提交表单发现服务器没有响应,那么用户可能以为是自己没有提交表单,则会再次点击提交按钮来重复提交表单
所以在开发中,我们必须防止这一情况。
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
<title>Form表单</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
用户名:<input type="text" name="username">
<input type="submit" value="提交" id="submit">
</form>
</body>
</html>
form表单提交到DoFormServlet进行处理:
public class DoFormServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
try {
// 让当前的线程睡眠3秒钟来模拟网络延迟
Thread.sleep(3 * 1000);
}catch(Exception e) {
e.printStackTrace();
}
System.out.println("向数据库中插入数据:" + username);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
如果没有进行form表单重复提交处理,那么在网络延迟的情况下下面的操作将会导致form表单重复提交多次。
场景一:
在网络延迟的情况下用户有时间多次点击submit按钮导致表单重复提交。
场景二:
在表单提交后,用户点击刷新网页导致表单数据重复提交。
场景三:
用户提交表单后,点击浏览器的后退按钮回退到表单页面进行再次提交。
JS防止表单重复提交
防止表单重复提交比较常用的方式是采用JavaScirpt来防止表单重复提交,具体做法如下:
修改上方form.jsp页面,添加如下JavaScirpt代码来防止表单重复提交
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>form表单提交</title>
<script type="text/javascript">
var isCommited = false;
function doSubmit() {
if (isCommited == false) {
isCommited = true;
return true;
}else {
return false;
}
}
</script>
</head>
<body>
<form action="${pageContext.request.contextPath}/servlet/DoFormServlet"
onsubmit="return doSubmit()" method="post">
用户名<input type="text" name="username">
<input type="submit" value="提交" id="submit">
</form>
</body>
</html>
这样就即使多次点击提交按钮,也只会提交一次,就可以避免重复提交导致的问题。
Session防表单重复提交
在上方提到的三种重复提交的场景中,第二和第三种场景通过客户端无法解决,需要服务端利用session来解决这种重复提交的问题。
方案:
在服务端生成一个唯一的随机标识号,也就是Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的的Form表单中,在Form表单中使用隐藏域来存储该Token,表单提交的时候连同该Token一起提交到服务端,然后服务端对该Token进行比较是否一致。不一致则是重复提交,此时服务端就可以不处理重复提交的表单了。处理完后再清楚服务端当前用户的Session域中存储的标识号即可。
在下列情况下,服务器程序将拒绝处理用户提交的表单请求:
存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
当前用户的Session中不存在Token(令牌)。
用户提交的表单数据中没有Token(令牌)。
a) 创建FormServlet,用于生成Token令牌和跳转到form.jsp页面:
目前调试出现问题,下次再来调试记录一下......