JavaWeb 学习笔记 6:会话跟踪

JavaWeb 学习笔记 6:会话跟踪

HTTP 协议本身是无状态的,所以不能跟踪会话状态。所以会有额外的技术用于跟踪会话:

  • Cookie,客户端技术
  • Session,服务端技术

1.Cookie

1.1.写入 Cookie

可以在服务端通过HttpServletResponse.addCookie向浏览器写入 Cookie:

@WebServlet("/a")
public class ControllerA extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 向浏览器添加 cookie
        Cookie cookie = new Cookie("username", "icexmoon");
        Cookie cookie1 = new Cookie("msg", "hello");
        resp.addCookie(cookie);
        resp.addCookie(cookie1);
    }
}

请求 http://localhost:8080/session-demo/a 能看到响应报文头:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: username=icexmoon
Set-Cookie: msg=hello
Content-Length: 0
Date: Mon, 11 Sep 2023 09:39:41 GMT

使用开发者工具可以看到浏览器端 Cookie 已添加:

image-20230911174548444

应当注意到,服务端添加的 Cookie 默认的存活时间(Expire / Max age)默认是会话,即会话结束(关闭浏览器)后 Cookie 就会被销毁。此时 Cookie 仅保存在内存中,并不会被持久化保存(保存到硬盘)。

使用Cookie.setMaxAge可以设置 Cookie 的生存时间(单位:秒):

// 向浏览器添加 cookie
Cookie cookie = new Cookie("username", "icexmoon");
Cookie cookie1 = new Cookie("msg", "hello");
// 设置有效时间为 1 天
cookie.setMaxAge(1 * 24 * 60 * 60);
resp.addCookie(cookie);
resp.addCookie(cookie1);

响应报文:

Set-Cookie: username=icexmoon; Expires=Tue, 12-Sep-2023 09:54:25 GMT
Set-Cookie: msg=hello

响应报文中的 Cookie 有效期是直接以截至时间的方式返回的:

Expires=Tue, 12-Sep-2023 09:54:25 GMT

这是格林尼治时间(GMT),换算成中国时间(东八区)要+8小时。

用开发者工具查看就能看到有效期已经改变:

image-20230911175940484

有效期可以设置为以下几种:

  • 正数,在X秒后过期
  • 0,立即过期(删除)
  • 负数,会话有效期,在会话结束(浏览器退出)后过期

1.2.读取 Cookie

使用HttpServletRequest.getCookies可以读取浏览器传递的 Cookie:

@WebServlet("/b")
public class ControllerB extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Cookie[] cookies = req.getCookies();
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("username")) {
                String username = cookie.getValue();
                System.out.println("username: " + username);
                break;
            }
        }
    }
}

请求 http://localhost:8080/session-demo/b 就能看到服务端输出的 Cookie 内容。

查看请求报文:

GET /session-demo/b HTTP/1.1
Cookie: JSESSIONID=20C2014C72F0D7ED4D34B821B9A0BC89; username=icexmoon; msg=hello; sentinel_dashboard_cookie=69C1AF3B99482E641CDD23041937F691; JSESSIONID=6EEEF7C596140410E7A21F9DAECF4525
...

当前域名下的所有 Cookie 都以Cookie: xxx=xxx; xxx=xxx 这样的请求头传递。

1.3.中文 Cookie

HTTP 协议规定,报文头内容只能是 ASCII 字符集的字符,所以如果尝试写入中文的 Cookie 信息(UTF-8 字符集)就会报错:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cookie cookie = new Cookie("username", "魔芋红茶");
    response.addCookie(cookie);
}

错误信息:

java.lang.IllegalArgumentException: Control character in cookie value or attribute.

所以要将 UTF-8 字符串转换为全部由 ASCII 字符组成的字符串才能作为 Cookie 内容传递。有多种编码可以实现这一点,最常用的有 URL 编码和 Base64 编码。

这里用 URL 编码举例说明:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String username = "魔芋红茶";
    username = URLEncoder.encode(username, StandardCharsets.UTF_8.name());
    Cookie cookie = new Cookie("username", username);
    response.addCookie(cookie);
}

响应报文中的信息:

Set-Cookie: username=%E9%AD%94%E8%8A%8B%E7%BA%A2%E8%8C%B6

自然的,在服务端接收到的 Cookie 也是 URL 编码过的,所以需要解码:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String username = ServletUtil.getCookie(request, "username");
    if (username!=null){
        username = URLDecoder.decode(username, StandardCharsets.UTF_8.name());
    }
    System.out.println(username);
}

2.Session

Session 同样可以用于跟踪会话,并保存会话的状态信息,与 Cookie 不同的是,Session 是服务端技术,保存在服务端。

2.1.写入 Session

@WebServlet("/e")
public class ControllerE extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        session.setAttribute("msg", "Hello World!");
    }
}

2.2.读取 Session

@WebServlet("/f")
public class ControllerF extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        String msg = (String) session.getAttribute("msg");
        System.out.println("Msg in session: " + msg);
    }
}

2.3.实现原理

Session 是基于 Cookie 实现的,浏览器端持有的是作为 Cookie 存储的 SessionID,服务端为每个 SessionID 保存对应的 Session 对象,并且可以用浏览器端用 Cookie 方式传递的 SessionID 获取到对应的 Session 对象。

整个过程可以表示为:

session原理.drawio

根据 Session 的实现原理,Session 的有效期也包含两部分:

  • 浏览器端 SessionID 的有效期
  • 服务器端的 Session 对象的有效期

两者任意一个失效 Session 就不可用了。

浏览器端的 SessionID 的有效期是会话,即关闭浏览器后就失效:

image-20230912123211545

服务器端的 Session 对象由 Web 服务器软件的设置决定,对于 Tomcat,默认的设置为 30 分钟后被清理。需要说明的是,每次有当前会话的请求产生,对应的 Session 对象的过期时间就会刷新,即 +30 分钟。也就是说只要一直有请求,Session 就不会过期,但是如果有超过 30 分钟没有请求,那 Session 对象就会过期被删除。

之所以为 Session 对象设置有效期,是因为 Session 需要占用服务端内存资源。因此尽量不要为 Session 设置过长的有效期。

Tomcat 的默认设置在 /conf/web.xml 中:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

可以通过修改 Web 应用的 web.xml 覆盖 Tomcat 的默认设置:

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <session-config>
    <session-timeout>1</session-timeout>
  </session-config>
</web-app>

这样就可以将 Session 对象的过期时间修改为 1 分钟,1 分钟后再请求就会发现对应的 Session 对象已经获取不到了。

也可以用 HttpSession.invalidate方法主动让某个 Session 对象过期。

2.4.Session 的持久化

Session 对象是保存在内存中的,这意味着服务器重启后之前的 Session 对象将不存在。对此,Tomcat 可以在正常退出时将内存中的 Session 序列化后保存在硬盘上,再次启动后从硬盘加载 Session 对象到内存。

非常正常退出,比如关闭线程或者服务器电源关闭等无法持久化保存 Session。

下面用一个简单测试进行验证。

使用命令行mvn tomcat7:run启动 Web 项目。

请求 xxx/e后再请求xxx/f,可以看到 session 已经生成,并且可以读取。

在命令行中按Ctrl+C结束 Tomcat。

注意把 Session 有效期改回 30 秒,并去除相关主动销毁 Session 的代码。

此时会在 Tomcat 下的 localhost/session-demo/org 目录下出现一个序列化文件SESSIONS.ser

image-20230912175034988

重新启动 Tomcat 后,如果需要使用 Session,Tomcat 会将之前的 Session 对象从序列化文件加载,并删除该序列化文件,因此可以访问之前的 Session。

2.5.Session 和 Cookie 的区别

  • 存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端
  • 安全性:Cookie不安全,Session安全
  • 数据大小:Cookie最大3KB,Session无大小限制
  • 存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
  • 服务器性能:Cookie不占服务器资源,Session占用服务器资源

3.案例:登录注册

登录和验证的实现都比较简单,这里只说明一下验证码的实现。

这里使用一个工具类 CheckCodeUtil 实现验证码的生成:

public class CheckCodeUtil {
    /**
     * 输出随机验证码图片流,并返回验证码值(一般传入输出流,响应response页面端,Web项目用的较多)
     *
     * @param w 宽
     * @param h 高
     * @param os 输出流
     * @param verifySize 验证码位数
     * @return 生成的验证码(字符串)
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, os, verifyCode);
        return verifyCode;
    }
    // ...
}

利用这个工具类生成验证码,并将生成的验证码图片写入响应报文的输出流:

@WebServlet("/user/check_code")
public class CheckCodeController extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        OutputStream os = response.getOutputStream();
        String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
        request.getSession().setAttribute("checkCode", checkCode);
    }
    // ...
}

用于验证的字符串形式的验证码要保存到 Session,以便在收到注册请求时进行验证。

注册页面用于显示验证码的图片设置src

<tr>
    <td>验证码</td>
    <td class="inputs">
        <input name="checkCode" type="text" id="checkCode">
        <img id="checkCodeImg" src="/login-demo/user/check_code">
        <a href="#" id="changeImg" onclick="refreshCheckCode()">看不清?</a>
    </td>
</tr>

现在页面加载时就能显示验证码。为了能点击 看不清 链接时能刷新,需要实现一个替换图片 src 的 js 方法:

// 刷新验证码
function refreshCheckCode(){
    $("img#checkCodeImg").attr("src","/login-demo/user/check_code");
}

要注意的是,此时只有在开发者工具选择禁用缓存的情况下才能正常刷新验证码,缓存生效时是不会有效果的,因为验证码图片会被缓存起来,浏览器会直接使用缓存,不会再次请求。

这就需要让生成验证码图片的 Servlet 返回的响应报文中禁用缓存:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 浏览器不能缓存验证码 Cache-Control: no-cache
    response.setHeader("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate");
    // ...
}

现在就没有类似的问题了。

在客户端发起注册请求时检查验证码:

// 检查验证码是否正确
String checkCode = (String) request.getSession().getAttribute("checkCode");
String inputCheckCode = request.getParameter("checkCode");
if (checkCode == null || inputCheckCode == null){
    throw new RuntimeException("请先输入验证码");
}
if (!checkCode.equalsIgnoreCase(inputCheckCode)){
    System.out.println(checkCode);
    System.out.println(inputCheckCode);
    throw new RuntimeException("验证码不正确");
}

The End,谢谢阅读。

本文的完整示例可以从这里获取。

4.参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值