Java Servlet中Session的原理以及使用方式

详细介绍了Java Web中的Session技术的原理以及常见用法。

我们上次学习了Cookie的概念和常见用法:Java Web(9)—Cookie技术的原理以及用法,本次我们来学习Session!

Session和cookie的作用有点类似,都是为了存储用户相关的状态信息,是一种会话跟踪技术。不同的是,cookie是存储在本地浏览器,而session存储在服务器。
存储在服务器的数据会更加的安全,不容易被窃取,并且session比Cookie使用方便,Session可以存储对象,Cookie只能存储字符串,并且tomcat8.x之后的版本中Cookie可以存储的字符类型有限制。

我们常常使用的Session是javax.servlet.http.HttpSession

1 获取HttpSession对象

  1. HttpSession request.getSesssion()
    1. 调用该方法,并且是第一次请求的时候,服务器会创建一个Session对象并返回,并且每个Session对象都有一个唯一的JSESSIONID,随后会以Set-cookie响应头的形式将JSESSIONID返回给客户端,客户端再次访问的时候将JSESSIONID发送给服务器。后续请求的时候服务器可通过JSESSIONID找到对应的Session对象,如果当前会话已经有了Session对象那么getSesssion()方法直接返回当前的Session对象;
  2. HttpSession request.getSession(boolean)
    1. 当参数为true时,与requeset.getSession()相同。如果参数为false,那么如果当前会话中存在Session对象则返回,不存在返回null,而不是创建;

2 HttpSession是域对象

我们已经学习过HttpServletRequest、ServletContext,它们都是域对象,现在我们又学习了一个HttpSession,它也是域对象,范围是当前会话。它们三个是Servlet中可以使用的域对象,而JSP中可以多使用一个域对象pageContext

  1. HttpServletRequest:一个请求创建一个request对象,所以在同一个请求中可以共享request,例如一个请求从AServlet转发到BServlet,那么AServlet和BServlet可以共享request域中的数据;
  2. ServletContext:一个应用只创建一个ServletContext对象,所以在ServletContext中的数据可以在整个应用中共享,只要不关闭服务器,那么ServletContext中的数据就可以共享,作用域是整个应用程序;
  3. HttpSession:一个会话创建一个HttpSession对象,同一会话中的多个请求中可以共享session中的数据;作用域是当前会话,会话关闭Session消失。

凡是域对象,都有如下4个操作数据的方法:

方法描述
void setAttribute(String name, Object object)将对象绑定到此域对象中的给定属性名称。如果指定的名称已用于属性,则此方法将用新属性的值替换旧的属性。
Object getAttribute(String name)返回具有给定名称key的包含属性值的value对象,如果不存在与给定名称匹配的属性,则返回 null。
void removeAttribute(String name)从此域对象中删除具有给定名称的属性。删除后,对getattrit (java.lang.String) 以检索属性值的后续调用将返回 null。如果参数name指定的域属性不存在,那么本方法什么都不做。
Enumeration getAttributeNames()返回包含此域对象中可用的属性名称的枚举。如果没有则返回空枚举。

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

方法描述
String getId()获取sessionId
int getMaxInactiveInterval()获取session可以的最大不活动时间(秒),默认为30分钟。当session在30分钟内没有使用,那么tomcat会在session池中移除这个session;
void setMaxInactiveInterval(int interval)设置单个session允许的最大不活动时间(秒),如果设置为1秒,那么只要session在1秒内不被使用,那么session就会被移除;值为零或负数表示Session永不超时。
long getCreationTime()返回session的创建时间,返回值为当前时间的毫秒值;
long getLastAccessedTime()返回session的最后活动时间,返回值为当前时间的毫秒值;
void invalidate()调用这个方法会导致Session立即失效,当session失效后,客户端再次请求,服务器会给客户端创建一个新的session,并在响应中给客户端新session的sessionId
boolean isNew()查看session是否为新。当客户端第一次请求时,服务器为客户端创建session,但这时服务器还没有响应客户端,也就是还没有把sessionId响应给客户端时,这时session的状态为新。

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. 从存储方式上比较
    1. Cookie只能存储字符串,如果要存储非ASCII字符串还要对其编码。
    2. Session可以存储任何类型的数据,可以把Session看成是一个容器
  2. 从隐私安全上比较
    1. Cookie存储在浏览器中,对客户端是可见的。信息容易泄露出去。如果使用Cookie,最好将Cookie加密
    2. Session存储在服务器上,对客户端是透明的。不存在敏感信息泄露问题。
  3. 从有效期上比较
    1. Cookie保存在硬盘中,只需要设置maxAge属性为比较大的正整数,即使关闭浏览器,Cookie还是存在的
    2. Session的保存在服务器中,设置maxInactiveInterval属性值来确定Session的有效期。并且Session依赖于名为JSESSIONID的Cookie,该Cookie默认的maxAge属性为-1。如果关闭了浏览器,该Session虽然没有从服务器中消亡,但也就失效了。
  4. 从对服务器的负担比较
    1. Session是保存在服务器的,每个用户都会产生一个Session,如果是并发访问的用户非常多,是不能使用Session的,Session会消耗大量的内存。
    2. Cookie是保存在客户端的。不占用服务器的资源。像baidu、Sina这样的大型网站,一般都是使用Cookie来进行会话跟踪。
  5. 从浏览器的支持上比较
    1. 如果浏览器禁用了Cookie,那么Cookie是无用的了!
    2. 如果浏览器禁用了Cookie,Session可以通过URL地址重写来进行会话跟踪。
  6. 从跨域名上比较
    1. Cookie可以设置domain属性来实现跨域名
    2. 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学习博客!

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值