06-Cookie&Session

会话管理

  • 什么是会话?
    Browser 开始访问网站到访问网站结束期间产生的多次请求响应组合在一起叫做一次会话
  • 会话过程中要解决的一些问题?
    • 每个用户在使用浏览器与服务器进行会话的过程中,不可避免各自会产生一些数据,程序要想办法为每个用户保存这些数据。
    • 例如:用户点击超链接通过一个 BuyServlet 购买了一个商品,程序应该想办法保存用户购买的商品,以便于用户请求 PayServlet 时,PayServlet 可以得到用户购买的商品为用户结帐。
    • 思考:用户购买的商品保存在 request 或 servletContext 中行不行?
  • 保存会话数据的两种技术:Cookie、Session
    • Cookie是【客户端技术】,程序把每个 Client 的数据以 Cookie 的形式写给用户各自的 Browser。当 Client 使用的 Browser 再去访问 Server 中的 web 资源时,就会带着各自的数据去。这样,web 资源处理的就是 Client 各自的数据了
    • Session 技术是【服务器端技术】,利用这个技术,Sever 在运行时可以为每一个 Client 的 Browser 创建一个其独享的 Session 对象,由于 Session 为每个 Client-Browser 独享,所以用户在访问 Server 的 web 资源时,可以把各自的数据放在各自的 Session 中,当用户再去访问 Server 中的其他 web 资源时,其他 web 资源再从 Client 各自的 Session 中取出数据为 Client 服务

简述

Cookie是基于 [set-Cookie响应头] 和 [Cookie请求头] 工作的

  • Sever 可以发送 [set-Cookie响应头] 命令 Browser 保存一个 Cookie 信息
  • Browser 会在访问 Server 时以 [Cookie请求头] 的方式带去之前保存的信息

API

[构造器] public Cookie(String name, String value)
[常用方法]
    public String getName()
    public String getValue()
    public int getMaxAge()
    public String getPath()
    public String getDomain()
    public void setValue(String newValue)
    public void setMaxAge(int expiry)
    public void setPath(String uri)
    public void setDomain(String pattern)
[从请求中获取Cookie] Cookie[] cs = request.getCookies()
[向响应中添加Cookie] response.addCookie(c)

方法细节:

  • getName()
    • name属性只有get() 没有 set(),意味着名称在创建之后不得更改
    • 如果想要更改,就重新创建一个新的 Cookie
  • setMaxAge() / getMaxAge()
    • 一个 Cookie 如果没有设置过 MaxAge,则这个 Cookie 是一个"会话级别的Cookie",这个 Cookie 信息打给 Browser 后,Browser 会把它保存在 Browser 的内存中,这意味着,只要 Browser 一关闭,随着 Browser 占用的内存的销毁,Cookie 信息随之销毁
    • 设置过 MaxAge 的 Cookie 一旦被 Browser 收到,则会被 Browser 以文件的形式保存在 Browser 的临时文件夹中,保存到指定的时间到来为止,这样一来,即使多次开关 Browser,由于 Browser 都能在临时文件夹中看到 Cookie 文件,所以在 Cookie 失效之前,Cookie 信息都存在
  • setPath() / getPath()
    • 用来通知 Browser 在访问 Server 中的哪个路径及其子路径时带着当前 Cookie 信息过来
    • 默认为发送 Cookie 的 Servlet 所在的路径
    • Cookie 对于指定目录中的所有页面及该目录子目录中的所有页面都是可见的
  • setDomain() / getDomain()
    • 用来通知 Browser 在访问哪个域名的时候带着当前的 Cookie 信息(→ 第三方Cookie)
    • 但是,现在的 Browser 一旦发现 Cookie 设置过 domain 信息则会拒绝接受这个 Cookie

案例

最后访问时间

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
    response.setContentType("text/html;charset=utf-8");
    Cookie[] cs = request.getCookies();
    Cookie findC = null;
    if(cs != null)
        for(Cookie c : cs)
            if("lastTime".equals(c.getName()))
                findC = c;

    if(findC == null) {
        response.getWriter().write("第一次来啊");
    } else {
        String lastTime = findC.getValue();
        response.getWriter().write("上次访问时间: " + lastTime);
    }

    Cookie c = new Cookie("lastTime", System.currentTimeMillis()+"");
    // 本 Cookie 在 Client 保存一个月
    c.setMaxAge(3600 * 24 * 7);
    // 只要是访问 本web应用 都把这个 Cookie 带着
    c.setPath(request.getContextPath());
    response.addCookie(c);
}

最后看过的书

  • BookListServlet
    • 查找 DB 找出所有的书,打印给 Browser
    • 显示曾经看过的书
  • BookInfoServlet
    • 找出要看的书的详细信息打印给浏览器
    • 发送 Cookie,将当前的书作为曾经看过的书保存起来
public class BookListServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        
        // 1. 查找DB找出所有的书, 显示给browser
        response.getWriter().write("<h2>图书Top4:</h2>");
        Map<String,Book> map = BookDao.getBooks();
        for(Map.Entry<String, Book> entry : map.entrySet()) {
            Book book = entry.getValue();
            response.getWriter().write(String.format("<a href='%s'>%s</a><br>%s<br>"
                , request.getContextPath()+"/servlet/BookInfoServlet?id="
                    + book.getId(), book.getName(),book.getDesc()));
        }
        response.getWriter().write("<hr><h2>阅书记录:</h2>");

        // 2. 显示之前看过的多本书
        Cookie[] cs = request.getCookies();
        Cookie findC = null;
        if(cs != null)
            for(Cookie c : cs)
                if("last".equals(c.getName()))
                    findC = c;
        if(findC == null) {
            response.getWriter().write("你咋这么没文化,快来看书!");
        } else {
            String[] ids = findC.getValue().split(",");
            if(ids != null)
                for(String id : ids) {
                    Book book = BookDao.getBook(id);
                    response.getWriter().write(book.getName()+"<br/>");
                }
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}
public class BookInfoServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        
        // 1. 找出要看的书的详细信息打给browser
        String id = (String) request.getParameter("id");
        Book book = BookDao.getBook(id);
        if(book == null) {
            response.getWriter().write("can't find this book!");
        } else {
            response.getWriter().write(book.toString());
        }
        
        // 2. 发送Cookie将当前的书作为曾经看过的书保存起来
        /*
         *  1 ---> 1
         *  3 ---> 3,1
         *  4 ---> 4,3,1
         *  2 ---> 2,4,3
         *  3 ---> 3,2,4
         */
        StringBuffer ids = new StringBuffer();
        Cookie[] cs = request.getCookies();
        Cookie findC = null;
        if(cs != null)
            for(Cookie c : cs)
                if("last".equals(c.getName()))
                    findC = c;
        if(findC == null) { // 说明之前没有看过书的记录
            ids.append(book.getId());
        } else { // 说明有阅书记录,根据历史纪录再算出一个新的纪录
            ids.append(book.getId());
            String historyStr = findC.getValue();
            String[] historyArr = historyStr.split(",");
            for(int i = 0; i < historyArr.length; i++) {
                if(!historyArr[i].equals(book.getId()))
                ids.append("," + historyArr[i]);
                if(ids.toString().split(",").length == 3) break;
            }
        }
        Cookie lastC = new Cookie("last", ids.toString());
        lastC.setMaxAge(3600 * 24 * 1);
        lastC.setPath(request.getContextPath());
        response.addCookie(lastC);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}

记住用户名

LoginServlet

public class LoginServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        UserService service = new UserService();
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        // 获取用户名密码
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        // 调用service检查用户名密码
        User user = service.isUser(username, password);
        if(user == null) { // 不正确 提示
            request.setAttribute("msg", "用户名密码不正确");
            request.getRequestDispatcher("/login.jsp").forward(request, response);
            return;
        } else { // 正确
            // 向session添加登陆标记
            request.getSession().setAttribute("user", user);
            // 记住用户名
            if("yes".equals(request.getParameter("remName"))) {
                // IllegalArgumentException: Control character in cookie
                // 请求头不能包含中文
                Cookie remNameC = new Cookie("remName",
                        URLEncoder.encode(user.getUsername(), "UTF-8"));
                remNameC.setPath(request.getContextPath());
                remNameC.setMaxAge(3600*24*30);
                response.addCookie(remNameC);
                } else { // 不想记住用户名
                    Cookie remNameC = new Cookie("remName", null);
                    remNameC.setPath(request.getContextPath());
                    remNameC.setMaxAge(0);
                    response.addCookie(remNameC);
                }
            // 重定向回主页
            response.sendRedirect(request.getContextPath()+"/index.jsp");
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

URLDecoderTag

public class URLDecoderTag extends SimpleTagSupport {
    private String content;
    private String charset;

    @Override
    public void doTag() throws JspException, IOException {
        String str = URLDecoder.decode(content,
                (charset==null ? "utf-8" : charset));
        getJspContext().getOut().write(str);
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getCharset() {
        return charset;
    }

    public void setCharset(String charset) {
        this.charset = charset;
    }
}

WEB-INF/MyTag.tld

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
 http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
    <tlib-version>1.0</tlib-version>
    <short-name>MyTag</short-name>
    <uri>http://www.nuist.edu.cn/MyTag</uri>
    <tag>
        <name>URLDecoder</name>
        <tag-class>cn.edu.nuist.tag.URLDecoderTag</tag-class>
        <body-content>empty</body-content>
        <attribute>
            <name>content</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
            <type>java.lang.String</type>
        </attribute>
        <attribute>
            <name>charset</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
            <type>java.lang.String</type>
        </attribute>
    </tag>
</taglib>

login.jsp

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.nuist.edu.cn/MyTag" prefix="MyTag" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <title>登陆页面</title>
    </head>
    <body>
        <h1>我的网站-登陆</h1>
        <h3 style="color: red">${msg }</h3>
        <hr/>
        <form action="${pageContext.request.contextPath }/servlet/LoginServlet" method="post">
            <table border="1">
                <tr>
                    <td>用户名</td>
                    <td><input type="text" name="username" value='<MyTag:URLDecoder
                    content="${cookie.remName.value }" charset="utf-8"/>'/></td>
                </tr>
                <tr>
                    <td>密码</td>
                    <td><input type="password" name="password" /></td>
                </tr>
                <tr>
                    <td><input type="submit" value="登陆" /></td>
                    <td><input type="checkbox" name="remName" value="yes"
                            <c:if test="${cookie.remName != null }">
                                checked="checked"
                            </c:if>
                        />记住用户名
                    </td>
                </tr>
            </table>
        </form>
    </body>
</html>

细节

  • 一个WEB站点可以给 Browser 发送多个 Cookie,一个 Browser 也可以存储多个WEB站点提供的 Cookie
  • Browser 一般只允许存放 300 个 Cookie,每个站点最多存放 20 个 Cookie,每个 Cookie 的大小限制为 4KB
  • 如果创建一个 Cookie,并将它发送到 Browser,默认情况下它是一个会话级别的 Cookie(即存储在 Browser 的内存中),用户退出 Browser 之后即被删除。若希望 Browser 将该 Cookie 保存在磁盘上,则需要使用 maxAge,并给出一个以"秒"为单位的时间。将最大时效设为"0"或负值,则是命令 Browser 删除该 Cookie
    • 删除 Cookie 时,path 必须一致,否则不会删除
    • Browser 通过 Cookie 的 "name+path+domain" 来标识一个 Cookie
    • 所谓的删除,即是:覆盖 → 超时 → 删除

Session

是个域对象

  • 作用范围:当前会话范围内
  • 生命周期
    • 出生:当程序第一次调用到 request.getSession() 说明 Client 明确的需要用到 Session,此时创建出对应 Client 的 Session 对象
    • 死亡
      • 当 Session 超过 30min(默认) 无人使用,则认为 Session 超时,销毁这个 Session(这个时间可以在 web.xml 中进行修改:<session-config>30min</session-config>
      • 程序中明确的调用 session.invalidate(),立即杀死 Session
      • 当 Server 被非正常关闭,随着 JVM 的死亡而死亡
  • 作用:在会话范围内共享数据

补充:

  1. 如果 Server 正常关闭,还未超时的 Session 会被以文件的形式保存在 Server 的 work 目录下,这个过程叫做 [Session 的钝化]。下次再正常启动 Server 时,钝化的 Session 会被恢复到内存中,这个过程叫做 [Session 的活化]。
  2. getSession(boolean create) 该 getSession 重载方法将返回与此请求关联的当前 HttpSession,如果没有当前会话并且 create 为 true,则返回一个新会话。如果 create 为 false 并且该请求没有有效的 HttpSession,则此方法返回 null。

实现原理

request.getSession() 会首先检查请求中是否有名为 JSESSIONID 的 Cookie:

  • 有,则拿出其值在 Server 中找到对应的 Session
  • 没有,再检查请求的 URL 后,有没有带着"JSESSIONID"
    • 如果有,则找到对应的 Session 为 Browser 服务
    • 如果还找不到,则认为 Browser 没有对应的 Session。创建一个 Session 然后再在响应中添加名为JSESSIONID 的 Cookie,值就是这个 Session 的 ID

Cookie — JSESSIONID:

  • Path 是当前 web 应用的名称
  • 是"会话级别"的 Cookie (没设过 maxAge),这意味着,一旦关闭 Browser 再重新打开,会由于 JSESSIONID 的丢失,找不到之前的 Session
  • 可以手动的发送 JSESSIONID Cookie 以延长 Session 的使用时间
    • 名字、ID 和 Path 与自动发送时的一样(覆盖 getSession() 底层自动发送的那个)
    • 不一样的是 maxAge,使 Browser 除了在内存中保存 JSESSIONID 信息以外,还在临时文件夹中以文件的形式保存。这样的话,即使重开 Browser 仍然可以使用之前的 Session
    • 代码实现
      HttpSession session = request.getSession();
      Cookie jc = new Cookie("JSESSIONID", session.getId());
      jc.setPath(request.getContextPath());
      jc.setMaxAge(1800);
      response.addCookie(jc);
      

URL 重写

当 Client-Browser 禁用 Cookie 后,Browser 就没办法接收 JSESSIONID Cookie 了,这样的话,Client 就用不了 Session 了。Server 标识 "browser-session" 的方法,就是通过 JSESSIONID-Cookie。

可以通过使用 "URL重写" 的机制,在所有的超链接后都拼接 JSESSIONID 信息,从而在点击超链接时,使用 URL 编码的方式带回 JSESSIONID 到 Server,从而使用 Session(getSession() 底层在找不到 JSESSIONID-Cookie 情况下,自动会去 URL 后检查是否携带该信息)。

response 提供的相关 API:

  • public String encodeURL(String url) 通过将会话 ID 包含在指定 URL 中对该 URL 进行编码,如果不需要编码,则返回未更改的 URL。此方法的实现包含可以确定会话 ID 是否需要在 URL 中编码的逻辑。例如,如果浏览器支持 cookie,或者关闭了会话跟踪,则 URL 编码就不是必需的。
  • public String encodeRedirectURL(String url) 对指定 URL 进行编码,以便在 sendRedirect 方法中使用它,如果不需要编码,则返回未更改的 URL。此方法的实现包含可以确定会话 ID 是否需要在 URL 中编码的逻辑。因为进行此确定的规则可能不同于用来确定是否对普通链接进行编码的规则,所以此方法与 encodeURL 方法是分开的。

补充:在"URL重写"方法调用之前,一定要先调用"request.getSession()",让session先被创建出来,这样才能有SessionID 以及之后的 "URL重写" 操作

<body>
    <%
        // 重写前, 要先获取出本次会话的JSessionId
        request.getSession();
        // URL 重写
        String url1 = request.getContextPath()+"/servlet/BuyServlet?prod=电视机";
        url1 = response.encodeURL(url1);
        String url2 = request.getContextPath()+"/servlet/BuyServlet?prod=洗衣机";
        url2 = response.encodeURL(url2);
        String url3 = request.getContextPath()+"/servlet/PayServlet";
        url3 = response.encodeURL(url3);
    %>
    <a href="<%=url1 %>">电视机</a>
    <a href="<%=url2 %>">洗衣机</a>
    <a href="<%=url3 %>">结账</a>
</body>

Quiz:Browser 没有禁用 Cookie,但是第一次访问的时候,查看页面源码,还是 URL 重写过的,但是页面一刷新拼接的 jessionid 就没了,为什么?

"URL重写" 方法底层,会先检查 Browser 的请求信息,如果发现 Browser 带去了任意 Cookie 信息到 Server,则认为 Client 没有禁用 Cookie,就不会再进行重写操作了

案例

用户注销

public class LogoutServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 1. 杀死Session
        if(request.getSession(false) != null
                && request.getSession().getAttribute("user") != null)
            request.getSession().invalidate();

        // 2. 重定向到主页
        response.sendRedirect(request.getContextPath() + "/index.jsp");
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}

防止表单重复提交

index.jsp

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <title>防止表单重复提交</title>
        <!--
        <script type="text/javascript">
            var isNotSub = true;
            function canSub() {
                if (isNotSub) {
                    isNotSub = false;
                    return true;
                } else {
                    alert("请不要重复提交!");
                    return false;
                }
            }
        </script>
        -->
    </head>
    <body>
        <%
            Random r = new Random();
            int valiNum = r.nextInt();
            if (session.getAttribute("valiNum") == null)
                session.setAttribute("valiNum", valiNum+"");
        %>
        <form action="${pageContext.request.contextPath }/servlet/ReSubServlet"
                method="post" onsubmit="return canSub()">
            <input type="text" name="username"/>
            <input type="hidden" name="valiNum" value="<%=valiNum %>"/>
            <input type="submit" value="注册"/>
        </form>
    </body>
</html>

ReSubServlet

public class ReSubServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");

        // 模拟网络延迟
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String username = request.getParameter("username");
        String valiNumFromClient = request.getParameter("valiNum");
        String valiNumFromSession = (String) request.getSession().getAttribute("valiNum");

        if (valiNumFromSession != null && valiNumFromSession.equals(valiNumFromClient)) {
            request.getSession().removeAttribute("valiNum");
            System.out.println("向 DB 中注册一次 —— " + username);
        } else {
            response.getWriter().write("From Server: 不要重复提交!");
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}

小结

  • Cookie 是客户端技术
    • 优点:数据保存在 Client,这个信息可以保存很长时间
    • 缺点
      • 数据随时有可能被清空,所以 Cookie 保存的数据不靠谱
      • 数据被保存在了 Client,随时有可能被人窃取,如果将一些敏感信息(用户名密码)存在 Cookie 中,可能会有安全问题
  • Session 是服务器端技术
    • 优点:数据保存在服务器端,相对来说比较稳定和安全
    • 缺点:占用 Server 内存,所以一般存活的时间不会太长,超过 "超时时间" 就会被销毁

要根据 Server 的压力和 Session 的使用情况,合理设置 Session 的超时时间,既能保证 Session 的存活时间够用,同时不用的 Session 可以及时销毁,减少对服务器内存的占用。

原文地址:https://www.cnblogs.com/liujiaqi1101/p/13388594.html