会话技术
会话:会话是指一个终端用户与交互系统进行通讯的过程。比如你用浏览器打开一个或多个标签,然后浏览信息,之后关闭浏览器或这个标签页,这个过程就叫做一次会话。
当用户通过浏览器访问web应用时,通常情况下,服务器需要对用户的状态进行跟踪,并且需要保存一些用户产生的数据,这就需要用到了会话技术。
Cookie
1、Cookie介绍
Cookie客户端技术,它用于将会话过程的数据保存到用户的浏览器。当用户再次访问服务器上的web资源时,就会带着各自的Cookie去访问,这样web应用就能知道当前是哪个用户正在操作。
1.1)Cookie由服务端产生,再发送给客户端保存,相当于本地缓存的作用。不是web内置对象,必须通过new的方式实例化。
1.2)服务器向客户端发送Cookie时,会在HTTP响应头字段中增加Set-Cookie
响应头字段。Set-Cookie头字段中设置的Cookie遵循一定的语法格式,具体示例如下。
Set-Cookie: key1=value1; key2=value2;...
Cookie必须以键值对的形式存在,属性可以有多个,但是属性之间必须用分号和空格隔开。
2、Cookie在浏览器和服务器之间的传输过程
当用户第一次访问服务器是,服务器会在响应消息中增加Set-Cookie字段,将用户信息以Cookie的形式发送给浏览器。一旦用户浏览器接受了服务器发送的Cookie信息,就会将它保存在浏览器的缓冲区中。这样当浏览器后续访问该服务器时,都会在请求消息中将用户信息以Cookie的形式发送给Web服务器,从而使服务器分辨出当前请求是由哪个用户发出的。
Cookie类常用API
方法 | 类型 | 描述 |
---|---|---|
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() | 成员方法 | 返回Cookie在浏览器上保存的有效秒数 |
public void setPath(String uri) | 成员方法 | 设置cookie的有效路径 |
public String getPath() | 成员方法 | 返回cookie的有效路径 |
注:
(1)当服务器给浏览器回送一个cookie时,如果在服务器端没有调用setMaxAge方法设置cookie的有效期,那么cookie的有效期只在一次会话过程中有效。当用户关闭浏览器,会话就结束了,此时cookie就会失效,如果在服务器端使用setMaxAge方法设置了cookie的有效期,比如设置了30分钟,那么当服务器把cookie发送给浏览器时,此时cookie就会在客户端的硬盘上存储30分钟,在30分钟内,即使浏览器关了,cookie依然存在,在30分钟内,打开浏览器访问服务器时,浏览器都会把cookie一起带上,这样就可以在服务器端获取到客户端浏览器传递过来的cookie里面的信息了。
当浏览器关闭再打开时,若希望Cookie有效,则必须为其设置setMaxAge。
浏览器的打开到关闭的为一次会话。
如果没有设置有效期,cookie首先是存在浏览器的缓存中的,当浏览器关闭时,浏览器的缓存自然就没有了,所以存储在缓存中的cookie自然就被清掉了。而如果设置了cookie的有效期,那么浏览器在关闭时,就会把缓存中的cookie写到硬盘上存储起来,这样cookie就能够一直存在了。
(2)Cookie不可以跨浏览器,每个浏览器都有自己的Cache。
(3)setPath(String path)
:如果创建的某个Cookie对象没有设置Path属性,那么该Cookie只对当前访问路径所属的目录及其子目录有效。如果想让某个Cookie项对站点的所有目录下的访问路径都有效,应该设置Path为"/"。
比如把cookie的有效路径设置为"/xdp",那么浏览器访问"xdp"目录下的web资源时,都会带上cookie,再比如把cookie的有效路径设置为"/xdp/gacl",那么浏览器只有在访问"xdp"目录下的"gacl"这个目录里面的web资源时才会带上cookie一起访问,而当访问"xdp"目录下的web资源时,浏览器是不带cookie的。
(4)Cookie的发送:response.addCookie(Cookie cookie)
方法将Cookie发送给客户端。response.addCookie(Cookie cookie)
会在响应头增加一个Set-Cookie头字段。
Cookie的查看:HttpServletRequest#getCookies
,可以用来查看客户端所传递过来的Cookie。
显示用户上次访问时间
public class LastAccessServlet extends HttpServlet {
private static String COOKIE_KEY = "lastAccess";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Content-Type", "text/html;charset=utf-8");
String lastAccessTime = null;
Cookie[] cookies = req.getCookies();
for (int i = 0; cookies != null && i < cookies.length; i++) {
if (COOKIE_KEY.equals(cookies[i].getName())) {
// 如果cookie的名称为lastAccess,则获取cookie的值
lastAccessTime = cookies[i].getValue();
}
}
if (lastAccessTime == null) {
resp.getWriter().write("您是首次访问本站!!!");
} else {
resp.getWriter().write("您上次访问的时间是:" + lastAccessTime);
}
LocalDateTime now = LocalDateTime.now();
// 创建一个Cookie
Cookie cookie = new Cookie(COOKIE_KEY, now.toString());
// 将cookie对象添加到response对象中,这样服务器在输出response对象中的内容时就会把cookie也输出到客户端浏览器
resp.addCookie(cookie);
}
}
resp.getWriter().write()
方法会向向浏览器中发送数据,但是该语句之后的程序段还是会正常执行呢,也就是说只有当doGet()
或者doPost()
方法执行完毕一次响应才算完成。
上述Cookie只能在一次会话中有效,当关闭浏览器时,Cookie失效。要想Cookie在关闭浏览器时依然有效,必须设置它的保留时长。
// 设置Cookie一天有效
cookie.setMaxAge(24*60*60);
response.addCookie(cookie);
chrome浏览器的cookie保存位置在C:\Users\username\AppData\Local\Google\Chrome\User Data\Default
目录下,里面有一个Cookie的文件,其实是一个SQLite文件,使用Navicate新建一个SQLite的连接,将其导入,即可查看。
Cookie注意细节
- 一个Cookie只能标识一种信息,它至少含有一个标识该信息的名称(NAME)和设置值(VALUE)。
- 一个WEB站点可以给一个WEB浏览器发送多个Cookie,一个WEB浏览器也可以存储多个WEB站点提供的Cookie。
- 浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB。
- 如果创建了一个cookie,并将他发送到浏览器,默认情况下它是一个会话级别的cookie(即存储在浏览器的内存中),用户退出浏览器之后即被删除。若希望浏览器将该cookie存储在磁盘上,则需要使用maxAge,并给出一个以秒为单位的时间。将最大时效设为0则是命令浏览器删除该cookie。
删除Cookie
注意:删除cookie时,path必须一致,否则不会删除
Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");
//将cookie的有效期设置为0,命令浏览器删除该cookie
cookie.setMaxAge(0);
response.addCookie(cookie);
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对象
什么是Session?
Session是一种将会话数据保存到服务器端的技术。当浏览器访问Web服务器时,Servlet容器就会创建一个Session对象和ID属性,为每一个对象提供了标识,当客户端后续访问服务器时,只要根据Cookie中的JSESSIONID
属性就能判断该会话属于哪一个用户。
Session和Cookie的区别
- Session是内置对象,可以通过
request.getSession()
属性获得,保存在服务器端,Cookie需要在服务器端实例化,通过response.addCookie(cookie)
将Cookie发送给客户端,保存在浏览器中的缓存中。 - Session需要Cookie或者重写URL来维持会话。由于客户端需要接收、记录和回送Session对象的ID,通常情况下,Session是借助Cookie技术来传递ID属性的。
HTTPSession API
session对象是与每个请求消息紧密相关的,Session的获取是通过HttpServletRequest
常用:HttpSession getSession();
/*
* true:返回当前会话对象,如果不存在,则新建一个
* false:返回当前会话对象,不存在,则返回NULL
*/
HttpSession getSession(boolean create);
/*
* 返回当前会话对象,如果不存在,则新建一个,相当于
* getSession(true);
*/
HttpSession getSession();
接口类分析
public interface HttpSession {
/**
* 返回sessionID
*/
public String getId();
/**
* 返回session所属的ServletContext
*/
public ServletContext getServletContext();
/**
* 设置当前会话的默认超时时间间隔
* 0或者负数表示当前会话永不超时
* 还可以在web.xml文件中通过<session-config><session-timeout>设置
*/
public void setMaxInactiveInterval(int interval);
/**
* 返回当前会话默认的超时时间间隔
*/
public int getMaxInactiveInterval();
/**
* 用于从当前HttpSession对象中返回指定名称的属性对象
*/
public Object getAttribute(String name);
/**
* 返回session对象中存储的所有属性名称的Enumeration对象
*/
public Enumeration<String> getAttributeNames();
/**
* 用于将一个对象与一个名称关联后存储到当前session中
* If the value passed in is null, this has the same effect as calling
* <code>removeAttribute()</code>.
*/
public void setAttribute(String name, Object value);
/**
* 将特定名称关联的对象删除
*/
public void removeAttribute(String name);
/**
* session失效,绑定到该对象上的所有属性失效
*/
public void invalidate();
/**
* 判断当前session是否是一个全新的session
* 如果客户端尚不了解会话或客户端选择不加入会话,则返回true。
* 例如,如果服务器仅使用基于cookie的会话,而客户端已禁用cookie的使用,
* 则在每个请求上会话都是新的。
*/
public boolean isNew();
/**
* Returns the last time the client sent a request associated with this
* session, as the number of milliseconds since midnight January 1, 1970
* GMT, and marked by the time the container received the request.
*/
public long getLastAccessedTime();
/**
* Returns the time when this session was created, measured in milliseconds
* since midnight January 1, 1970 GMT.
*/
public long getCreationTime();
}
session的超时管理:当用户第一次访问时,Web服务器就会创建一个与该客户端对应的HttpSession对象。在HTTP协议中,web服务器无法判断当前的客户端浏览器是否还会继续访问,所以即使客户端关闭浏览器,对于服务器来说是不知道的,这样就会导致session对象越来越多,对服务器的内存产生一定压力。为了有效地管理session,引入超时管理。如果当用户在一定时间内,没有使用到session对象,那么web服务器就会使该session对象失效。要想使session失效,除了可以等待会话时间超时外,还可以通过invalidate()
方法使session强制失效。
实例:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Content-Type", "text/html;charset=utf-8");
HttpSession session = req.getSession();
session.setAttribute("name", "Stronger");
if (session.isNew())
resp.getWriter().write("session创建成功,sessionID是:" + session.getId());
else {
resp.getWriter().write(session.getAttribute("name") + "session已经存在,sessionID是:" + session.getId());
}
}
第一次访问和第二次访问获得的sessionID为同一个,如果将Cookie禁用后,每次session对象都是一个新创建的。
Cookie禁用问题
如果浏览器的Cookie功能被禁止,那么服务器是无法通过Session保存用户会话信息的。
解决办法:允许Cookie、URL重写。
解决方案:URL重写
URL重写:将Session的会话标识号以参数的形式附加在超链接的URL地址后面,将JSESSIONID为关键字作为参数名,会话标识号作为参数值附加到URL地址后面。
HttpServletResponse
接口中定义了两个URL重写的方法:
/**
* 用于对表单action和超链接的url地址进行重写
*/
public String encodeURL(String url);
/**
* 用于对sendRedirect方法的URL进行编码
* 对指定的URL进行编码以供<code> sendRedirect </ code>方法使用,或者,
* 如果不需要编码,则返回不变的URL。该方法的实现包括确定会话ID是否需要在URL中编码的逻辑。
* 因为进行此确定的规则可能不同于用于决定是否对常规链接进行编码的规则,
* 所以此方法与<code> encodeURL </ code> 方法分开。
* 发送到<code> HttpServletResponse.sendRedirect </ code>方法的所有URL 应该通过此方法运行。
* 否则,URL重写不能与不支持Cookie的浏览器一起使用。
*/
public String encodeRedirectURL(String url);
注意点:
1)encodeRedirectURL
方法的调用前必须调用getSession
方法。
2)服务器根据浏览器端的Cookie是否被禁用来判断是否需要进行URL重写,如果客户端没有禁用Cookie,那么encodeRedirectURL
返回原始url,否则返回重写的URL,例如重写的形式为:http://localhost:8080/web/session;jsessionid=81BE96BE51F227CCB6043F00D6FA33D8
。URL重写后,服务端的getSession()
方法就不会新创建session了,每次都是同一个。
我的理解是tomcat容器解析到了客户端传递过来的
JSESSIONID
。
第一次请求
C--->S : request header没有Cookie,但这时,Server已经产生了一个SessionID,并返回给了C端
第二次请求:
C--->S : request header Cookie:JESSIONID=0123NDAWxxxx;
用户登录功能
最终效果演示:
涉及到的功能点有:
1)验证码
2)自动登录
3)防止表单重复提交
用户登录功能的交互过程图
首页请求
LoginIndex的实现
public class LoginIndex extends HttpServlet {
/**
* Cookie校验实现自动登录功能
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie != null && cookie.getName().equals(LoginCheck.FLAG_COOKIE)) {
String userInfo = cookie.getValue();
String username = URLDecoder.decode(userInfo.split("-")[0], "utf-8");
String password = userInfo.split("-")[1];
if (LoginCheck.DEFAULT_NAME.equals(username) && LoginCheck.DEFAULT_PWD.equals(password))
resp.getWriter().write("自动登录成功!");
return;
}
}
}
resp.sendRedirect(req.getContextPath() + "/login.html");
}
}
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="check" method="post" onsubmit="return checkNoRepeat()">
用户名:<input name="username" type="text"> <br>
密 码:<input name="password" type="password"> <br>
自动登录:<input name="autologin" type="checkbox" value="true"> <br>
验证码:<input name="verifyCode" type="text">
<img src="code" onclick="this.src='code?'+Math.random()"/> <br>
<input type="submit" value="提交" id="bt">
</form>
<script>
var repeat = false;
function checkNoRepeat() {
if (!repeat) {
repeat = true;
return true;
} else {
return false;
}
}
</script>
</body>
</html>
验证码
功能实现:自定义一个输出image的Servlet绑定到img标签的的src属性上面,验证码的切换,依靠jsonclick
方法,通过this对象更改属性值即可。
这里code后面跟上了一个随机数,如果img标签修改后的src属性和原来的相同,那么img属性不会去做更新,也就是不会去请求src的地址。
验证码类VerifyCode
public class VerifyCode extends HttpServlet {
private static int WIDTH = 60;
private static int HEIGHT = 20;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
resp.setContentType("text/html;charset=utf-8");
//设置浏览器不要缓存此图片
resp.setHeader("Pragma", "no-cache");
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("Expires", "0");
//创建内存图像,并获得内存上下文
BufferedImage bufferedImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics g = bufferedImage.getGraphics();
//产生随机的验证码
char[] rands = generateCheckCode();
//产生图像
drawBackground(g);
drawRands(g, rands);
//结束图像的绘制过程
g.dispose();
//将图像输出到客户端
ImageIO.write(bufferedImage, "JPEG", resp.getOutputStream());
//将当前验证码存入session
String code = new String(rands);
session.setAttribute("verifyCode", code);
}
private char[] generateCheckCode() {
//定义验证码的字符表
String chars = "0123456789abcdefghijklmnopqrstuvwxyz";
char[] rands = new char[4];
for (int i = 0; i < rands.length; i++) {
int rand = (int) (Math.random() * 36);
rands[i] = chars.charAt(rand);
}
return rands;
}
/**
* 画验证码
* @param g
* @param rands
*/
private void drawRands(Graphics g, char[] rands) {
g.setColor(Color.BLACK);
g.setFont(new Font(null, Font.ITALIC| Font.BOLD, 18));
//在不同的高度上输出验证码的每个字符
g.drawString("" + rands[0], 1, 17);
g.drawString("" + rands[1], 16, 15);
g.drawString("" + rands[2], 31, 18);
g.drawString("" + rands[3], 46, 16);
}
/**
* 画背景
* @param g
*/
private void drawBackground(Graphics g) {
g.setColor(new Color(0xDCDCDC));
g.fillRect(0, 0, WIDTH, HEIGHT);
//随机产生120个干扰点
for (int i = 0; i < 120; i++) {
int x = (int) (Math.random() * WIDTH);
int y = (int) (Math.random() * HEIGHT);
int red = (int) (Math.random() * 255);
int green = (int) (Math.random() * 255);
int blue = (int) (Math.random() * 255);
g.setColor(new Color(red, green, blue));
g.drawOval(x, y, 1, 0);
}
}
}
登录校验功能的实现 LoginCheck
public class LoginCheck extends HttpServlet {
public static String FLAG_COOKIE = "user";
public static String DEFAULT_NAME = "猩猩";
public static String DEFAULT_PWD = "qqq";
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 模拟延时,验证防重复提交表单的功能
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
HttpSession session = req.getSession();
String realCode = (String) session.getAttribute("verifyCode");
String inputCode = req.getParameter("verifyCode");
if (!realCode.equals(inputCode)) {
resp.getWriter().write("incorrect code");
return;
}
String username = req.getParameter("username");
String password = req.getParameter("password");
if (DEFAULT_NAME.equals(username) && DEFAULT_PWD.equals(password)) {
// 用来判断表单是否重复提交
System.out.println("用户名:" + username + " 密码:" + password);
resp.getWriter().write("登录成功->" + "用户名:" + username + " 密码:" + password);
String autoLogin = req.getParameter("autologin");
if (Boolean.parseBoolean(autoLogin)) {
resp.getWriter().write("<br>开启自动登录功能");
//将用户名和密码信息保存进cookie(实际需要加密处理)
// 中文字符需要编码处理
String user = URLEncoder.encode(DEFAULT_NAME, "utf-8") + "-" + DEFAULT_PWD;
Cookie cookie = new Cookie(FLAG_COOKIE, user);
resp.addCookie(cookie);
}
} else {
resp.getWriter().write("登录失败");
session.invalidate();
}
}
}
逻辑处理都在代码中了,这里不再介绍。
防止重复提交表单
在平时开发中,如果网速比较慢的情况下,用户提交表单后,发现服务器半天都没有响应,那么用户可能会以为是自己没有提交表单,就会再点击提交按钮重复提交表单,我们在开发中必须防止表单重复提交。
表单重复提交的常见场景:
1、在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交
2、表单提交后用户点击【刷新】按钮导致表单重复提交
3、用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交
防止表单重复提交的方式
1、采用JavaScript来防止表单重复提交
设置一个标识变量,结合form对象的onsubmit
方法,当用户点击submit类型的input组件时,执行onsubmit
方法,根据方法的返回值判断是否请求。
var repeat = false;
function checkNoRepeat() {
if (!repeat) {
repeat = true;
return true;
} else {
return false;
}
}
2、设置提交按钮不可用或者隐藏。
将按钮设置为隐藏,可能会让用户误以为是bug,在开发中常用的是将按钮设置为不可用。
3、利用Session防止表单重复提交
具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。
在下列情况下,服务器程序将拒绝处理用户提交的表单请求:
- 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
- 当前用户的Session中不存在Token(令牌)。
- 用户提交的表单数据中没有Token(令牌)。