Java Servlet中Session的原理以及使用方式
本次我们来学习Session!
Session和cookie的作用有点类似,都是为了存储用户相关的状态信息,是一种会话跟踪技术。不同的是,cookie是存储在本地浏览器,而session存储在服务器。 存储在服务器的数据会更加的安全,不容易被窃取,并且session比Cookie使用方便,Session可以存储对象,Cookie只能存储字符串,并且tomcat8.x之后的版本中Cookie可以存储的字符类型有限制。
我们常常使用的Session是javax.servlet.http.HttpSession!
1 获取HttpSession对象
- HttpSession request.getSesssion() 调用该方法,并且是第一次请求的时候,服务器会创建一个Session对象并返回,并且每个Session对象都有一个唯一的JSESSIONID,随后会以Set-cookie响应头的形式将JSESSIONID返回给客户端,客户端再次访问的时候将JSESSIONID发送给服务器。后续请求的时候服务器可通过JSESSIONID找到对应的Session对象,如果当前会话已经有了Session对象那么getSesssion()方法直接返回当前的Session对象;
- HttpSession request.getSession(boolean) 当参数为true时,与requeset.getSession()相同。如果参数为false,那么如果当前会话中存在Session对象则返回,不存在返回null,而不是创建;
2 HttpSession是域对象
我们已经学习过HttpServletRequest、ServletContext,它们都是域对象,现在我们又学习了一个HttpSession,它也是域对象,范围是当前会话。它们三个是Servlet中可以使用的域对象,而JSP中可以多使用一个域对象pageContext。
- HttpServletRequest:一个请求创建一个request对象,所以在同一个请求中可以共享request,例如一个请求从AServlet转发到BServlet,那么AServlet和BServlet可以共享request域中的数据;
- ServletContext:一个应用只创建一个ServletContext对象,所以在ServletContext中的数据可以在整个应用***享,只要不关闭服务器,那么ServletContext中的数据就可以共享,作用域是整个应用程序;
- HttpSession:一个会话创建一个HttpSession对象,同一会话中的多个请求中可以共享session中的数据;作用域是当前会话,会话关闭Session消失。
凡是域对象,都有如下4个操作数据的方法:
3 Session的实现原理
Session的实现是依赖Cookie的。
当某个客户端首次调用getSesssion()方法或者getSession(true)方法,服务器端要创建一个Session对象,返回给客户端的是该Session关联的一个唯一的SessionId(tomcat服务器是通过SessionIdGenerator产生的一个伪随机数),即JSESSIONID,它是通过Set-Cookie的形式发送给客户端的,因此在客户端仅仅的是以Cookie的形式保存了SessionId,而Session数据是保存在服务器的Session对象中。
如何判断某个客户端是不是首次调用getSesssion()方法或者getSession(true)方法?实际上就是根据该客户端的请求头中是否存在JSESSIONID的Cookie以及该JSESSIONID对应的Session是否存在(可能已经过期)。
当客户端再次访问服务器时,在请求中会带上SessionId(JSESSIONID)的Cookie,而服务器会通过请求头的Cookie中的名为JSESSIONID的key的SessionId找到对应的Session并返回,而无需再创建新的Session。
所以说,Session的实现是依赖Cookie的,并且客户端数据(状态信息)保存在服务器端。
另外,访问JSP动态资源时,将会自动创建Session,因为JSP在编译为Java文件之后,在代码中具有getSesssion()方法。
4 Session的超时时间
tomcat服务器默认对于创建Session的数量没有限制,为了防止内存溢出,服务器会把长时间没有活跃的Session从内存中删除,这个时间也就是Session的超时时间,默认是30分钟。如果你打开网站的一个页面开始长时间不动,超出了30分钟后,再去点击链接或提交表单时你会发现你的身份已经过期,因为你的Session已经丢失了!
但是在30分钟(超时时间)之内,只要继续访问(读/写)这个Session,服务器就会更新Session的最后访问时间,那么将超时时间将向后推移。
服务器的Session的默认超时时间,可以在${CATALANA}/conf/web.xml配置文件中设置,单位是分钟,,值为零或负数表示Session永不超时:
单个Web应用的Session的默认超时时间,可以在应用的web.xml的session-config标签中设置,如果没有设置,那么将会以服务器的时间为准,单位是分钟,值为零或负数表示Session永不超时,如下案例:
对于单个Session对象,可以通过在代码中调用setMaxInactiveInterval方法设置超时时间,单位是秒,值为零或负数表示Session永不超时。
5 session其他常用API
6 SessionId的生命
Session保存在服务器中,而SessionId通过Cookie发送给客户端,但我们知道Cookie的默认生命是-1,即只在浏览器内存中存在,也就是说如果用户关闭了浏览器,那么这个Cookie就丢失了。
当客户下一次打开浏览器访问的时候,虽然可能此时服务器端的Session还没有过期,但是由于没有了SessionId,那么此前的Session就找不到了,因此客户的状态可能就丢失了,可能就又需要重新登录。
对此,我们可以手动设置“JSESSIONID”的Cookie的过期时间,比如:
@WebServlet("/session-servlet")
public class SessionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
HttpSession session = req.getSession();
Cookie jsessionid = new Cookie("JSESSIONID", session.getId());
jsessionid.setMaxAge(30 * 60);
jsessionid.setPath("/");
resp.addCookie(jsessionid);
}
}
因此,通常Cookie和Session都会结合使用!
7 URL重写
如果客户端浏览器禁用了Cookie,那么浏览器就不会把Cookie带过去给服务器。对此,可以使用URL重写,将JSESSIONID直接加在请求参数后面,这样服务器可以通过获取JSESSIONID这个请求参数来得到客户端的SessionId,找到sessoin对象。
可以使用response对象调用encodeURL()或encodeRedirectURL()方法实现URL重写。在使用重定向时,需要使用encodeRedirectURL()方法。
这两个方法首先会判断当前的Servlet是否执行了HttpSession.invalidate()方法(当前session是否失效,失效后会重新建立新的session),如果已经执行返回参数URL,接下来判断客户端是否禁用了Cookie,没有禁用直接返回参数URL,如果禁用,则在URL参数中附加JSESSIONID,返回编码后的URL。
重定向的Servlet:
@WebServlet("/url-rewrite")
public class UrlRewrite extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
HttpSession session = req.getSession();
System.out.println("url-rewrite: "+session.getId());
//普通重定向
resp.sendRedirect("UrlRewrite-servlet");
//URL重写的重定向
// resp.sendRedirect(resp.encodeRedirectURL("UrlRewrite-servlet"));
}
}
目的Servlet:
@WebServlet("/UrlRewrite-servlet")
public class UrlRewriteServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
HttpSession session = req.getSession();
System.out.println("UrlRewrite-servlet: " + session.getId());
System.out.println();
}
}
我们尝试将浏览器的Cookie禁用(我这里是火狐浏览器):
访问“/url-rewrite”,首先使用普通重定向,控制台两个Servlet将会输出不同的Id:
接下来,我们对于UrlRewrite 使用resp.sendRedirect(resp.encodeRedirectURL("UrlRewrite-servlet"))进行带有URL重写的重定向,结果如下:
可以发现每次重定向都使用了相同的JSESSIONID,并且我们在浏览器的请求URL参数后面可以找到携带的JSESSIONID:
如果此时在其他没有禁用Cookie的浏览器上访问, 则URL后面不会携带JSESSIONID,这就是URL重写会动态的判断客户端是否禁用了Cookie!
注意:由于附加在URL中的sessionId是动态产生的,对每一个用户是不同的,所以对于静态页面的相互跳转(比如HTML),URL重写机制无能为力。当然可以通过将静态页面转换为动态页面(比如JSP)解决。
8 Session和Cookie的区别
1.从存储方式上比较
-
- Cookie只能存储字符串,如果要存储非ASCII字符串还要对其编码。
- Session可以存储任何类型的数据,可以把Session看成是一个容器
2.从隐私安全上比较
-
- Cookie存储在浏览器中,对客户端是可见的。信息容易泄露出去。如果使用Cookie,最好将Cookie加密
- Session存储在服务器上,对客户端是透明的。不存在敏感信息泄露问题。
3.从有效期上比较
-
- Cookie保存在硬盘中,只需要设置maxAge属性为比较大的正整数,即使关闭浏览器,Cookie还是存在的
- Session的保存在服务器中,设置maxInactiveInterval属性值来确定Session的有效期。并且Session依赖于名为JSESSIONID的Cookie,该Cookie默认的maxAge属性为-1。如果关闭了浏览器,该Session虽然没有从服务器中消亡,但也就失效了。
4.从对服务器的负担比较
-
- Session是保存在服务器的,每个用户都会产生一个Session,如果是并发访问的用户非常多,是不能使用Session的,Session会消耗大量的内存。
- Cookie是保存在客户端的。不占用服务器的资源。像baidu、Sina这样的大型网站,一般都是使用Cookie来进行会话跟踪。
5.从浏览器的支持上比较
-
- 如果浏览器禁用了Cookie,那么Cookie是无用的了!
- 如果浏览器禁用了Cookie,Session可以通过URL地址重写来进行会话跟踪。
6.从跨域名上比较
-
- Cookie可以设置domain属性来实现跨域名
- Session只在当前的域名内有效,不可夸域名
9 一次性图片验证码案例
生成验证码后,把验证码的数据存进Session域对象中,判断用户输入验证码是否和Session域对象的数据一致即可。并且在判断正确之后,需要防止重复提交,在实际开发过程中应该严格保证幂等性,下面的案例非常简单,仅作演示,不能在开发中使用。
图片验证码工具类:
public class ImageVerificationCode {
private int weight = 100; //验证码图片的长和宽
private int height = 30;
private String text; //用来保存验证码的文本内容
private Random r = new Random(); //获取随机数对象
//private String[] fontNames = {"宋体", "华文楷体", "黑体", "微软雅黑", "楷体_GB2312"}; //字体数组
//字体数组
private String[] fontNames = {"Georgia"};
//验证码数组
private String codes = "23456789abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ";
/**
* 获取随机的颜色
*
* @return
*/
private Color randomColor() {
int r = this.r.nextInt(225); //这里为什么是225,因为当r,g,b都为255时,即为白色,为了好辨认,需要颜色深一点。
int g = this.r.nextInt(225);
int b = this.r.nextInt(225);
return new Color(r, g, b); //返回一个随机颜色
}
/**
* 获取随机字体
*
* @return
*/
private Font randomFont() {
int index = r.nextInt(fontNames.length); //获取随机的字体
String fontName = fontNames[index];
int style = r.nextInt(4); //随机获取字体的样式,0是无样式,1是加粗,2是斜体,3是加粗加斜体
int size = r.nextInt(10) + 20; //随机获取字体的大小
return new Font(fontName, style, size); //返回一个随机的字体
}
/**
* 获取随机字符
*
* @return
*/
private char randomChar() {
int index = r.nextInt(codes.length());
return codes.charAt(index);
}
/**
* 画干扰线,验证码干扰线用来防止计算机解析图片
*
* @param image
*/
private void drawLine(BufferedImage image) {
int num = r.nextInt(10); //定义干扰线的数量
Graphics2D g = (Graphics2D) image.getGraphics();
for (int i = 0; i < num; i++) {
int x1 = r.nextInt(weight);
int y1 = r.nextInt(height);
int x2 = r.nextInt(weight);
int y2 = r.nextInt(height);
g.setColor(randomColor());
g.drawLine(x1, y1, x2, y2);
}
}
/**
* 创建图片的方法
*
* @return
*/
private BufferedImage createImage() {
//创建图片缓冲区
BufferedImage image = new BufferedImage(weight, height, BufferedImage.TYPE_INT_RGB);
//获取画笔
Graphics2D g = (Graphics2D) image.getGraphics();
//设置背景色随机
g.setColor(new Color(255, 255, r.nextInt(245) + 10));
g.fillRect(0, 0, weight, height);
//返回一个图片
return image;
}
/**
* 获取验证码图片的方法
*
* @return
*/
public BufferedImage getImage() {
BufferedImage image = createImage();
Graphics2D g = (Graphics2D) image.getGraphics(); //获取画笔
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 4; i++) //画四个字符即可
{
String s = randomChar() + ""; //随机生成字符,因为只有画字符串的方法,没有画字符的方法,所以需要将字符变成字符串再画
sb.append(s); //添加到StringBuilder里面
float x = i * 1.0F * weight / 4; //定义字符的x坐标
g.setFont(randomFont()); //设置字体,随机
g.setColor(randomColor()); //设置颜色,随机
g.drawString(s, x, height - 5);
}
this.text = sb.toString();
drawLine(image);
return image;
}
/**
* 获取验证码文本的方法
*
* @return
*/
public String getText() {
return text;
}
public static void output(BufferedImage image, OutputStream out) throws IOException //将验证码图片写出的方法
{
ImageIO.write(image, "JPEG", out);
}
}
生成验证码的Servlet:
/**
* @author lx
*/
@WebServlet("/VerifyCodeServlet")
public class VerifyCodeServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ImageVerificationCode vc = new ImageVerificationCode();
BufferedImage image = vc.getImage();
request.getSession().setAttribute("vCode", vc.getText());
ImageVerificationCode.output(image, response.getOutputStream());
}
}
注册的Servlet:
```java
@WebServlet("/RegisterServlet")
public class RegisterServlet extends HttpServlet {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
String username = request.getParameter("username");
String vCode = request.getParameter("code");
if (vCode.equalsIgnoreCase((String) request.getSession().getAttribute("vCode"))) {
//这里简单、分步的判断和删除不能保证原子性,可以借助redis
//LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
//成功之后将session删除,防止重复提交
request.getSession().removeAttribute("vCode");
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
System.out.println(username + ", 恭喜!注册成功!");
response.getWriter().print(username + ", 恭喜!注册成功!");
} else {
response.g
etWriter().print("验证码错误!");
System.out.println("验证码错误!");
}
}
}
页面:
<html lang="zh-CH">
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>JSP - Hello World</title>
<script type="text/javascript">
function _change() {
var imgEle = document.getElementById("vCode");
imgEle.src = "VerifyCodeServlet?" + new Date().getTime();
}
</script>
</head>
<body>
<form action="RegisterServlet" method="post">
用户名:<label>
<input type="text" name="username"/>
</label><br/>
验证码:<label>
<input type="text" name="code" size="3"/>
</label>
<img id="vCode" src="VerifyCodeServlet"/>
<a href="javascript:_change()">看不清,换一张</a>
<br/>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
作者:刘Java
链接:https://juejin.cn/post/7000570872490622990
来源:掘金