【问题标题】:Session or cookie confusion会话或 cookie 混淆
【发布时间】:2013-04-12 13:25:43
【问题描述】:

我在一些网站上看到用户登录他们的帐户然后关闭浏览器。

关闭并重新打开浏览器后,他们的帐户仍处于登录状态。

但有些网站,不能那样做。

我很困惑它被认为是会话还是 cookie?

如果我希望我的网站以这样的方式登录,是否必须设置session.setMaxInactiveInterval()cookie.setMaxAge()

【问题讨论】:

标签: session servlets cookies


【解决方案1】:

* 此答案存在严重缺陷,请参阅 cmets。 *


您的问题是关于会话跟踪

[第 1 部分]:会话对象

HTTP 请求是分开处理的,因此为了在每个请求之间保留信息(例如,有关用户的信息),必须在服务器端创建一个 会话对象

有些网站根本不需要会话。用户无法修改任何内容的网站不必管理会话(例如,在线简历)。在这样的网站上,您不需要任何 cookie 或会话。

创建会话:

在 servlet 中,使用来自 HttpServletRequest 对象的方法 request.getSession(true) 创建一个新的 HttpSession 对象。请注意,如果您使用request.getSession(false),如果会话尚未创建,则将返回 nullLook at this answer for more details.

设置/获取属性:

会话的目的是在每个请求之间保持服务器端的信息。例如,保留用户名:

session.setAttribute("name","MAGLEFF");
// Cast
String name = (String) session.getAttribute("name");

销毁会话:

如果长时间处于非活动状态,会话将被自动销毁。 Look at this answer for more details。但是您可以手动强制销毁会话,例如在注销操作的情况下:

HttpSession session = request.getSession(true); 
session.invalidate();

[第 2 部分]:所以...加入黑暗面,我们有 COOKIES 吗?

饼干来了。

JSESSIONID:

每次使用request.getSession() 创建会话时,都会在用户的计算机上创建一个JSESSIONID cookie。为什么?因为在服务器端创建的每个会话都有一个 ID。除非您没有正确的 ID,否则您无法访问其他用户的会话。这个 ID 保存在 JSESSIONID cookie 中,并允许用户找到他的信息。 Look at this answer for more details !

JSESSIONID 何时被删除?

JSESSIONID 没有过期日期:它是一个会话 cookie。与所有会话 cookie 一样,它会在浏览器关闭时被删除。如果使用基本的 JSESSIONID 机制,那么在关闭并重新打开浏览器后,会话将无法访问,因为 JSESSIONID cookie 已被删除。

请注意,客户端无法访问会话,但仍在服务器端运行。设置一个 MaxInactiveInterval 允许服务器在会话不活动时间过长时自动使会话无效。

对 JSESSIONID 的恶意破坏

只是为了好玩,有一天我在一个项目中发现了这段代码。它用于通过使用 javascript 删除 JSESSIONID cookie 来使会话无效:

<SCRIPT language="JavaScript" type="text/javascript">

    function delete_cookie( check_name ) {
        // first we'll split this cookie up into name/value pairs
        // note: document.cookie only returns name=value, not the other components
        var a_all_cookies = document.cookie.split( ';' );
        var a_temp_cookie = '';
        var cookie_name = '';
        var cookie_value = '';
        var b_cookie_found = false; // set boolean t/f default f
        // var check_name = 'JSESSIONID';
        var path = null;

        for ( i = 0; i < a_all_cookies.length; i++ )
        {
            // now we'll split apart each name=value pair
            a_temp_cookie = a_all_cookies[i].split( '=' );
            // and trim left/right whitespace while we're at it
            cookie_name = a_temp_cookie[0].replace(/^\s+|\s+$/g, '');
            // alert (cookie_name);

            // if the extracted name matches passed check_name
            if ( cookie_name.indexOf(check_name) > -1 )
            {
                b_cookie_found = true;
                // we need to handle case where cookie has no value but exists (no = sign, that is):
                if ( a_temp_cookie.length > 1 )
                {
                    cookie_value = unescape( a_temp_cookie[1].replace(/^\s+|\s+$/g, '') );
                    document.cookie = cookie_name + "=" + cookie_value +
                    ";path=/" +
                    ";expires=Thu, 01-Jan-1970 00:00:01 GMT";
                    // alert("cookie deleted " + cookie_name);
                }
            }
            a_temp_cookie = null;
            cookie_name = '';
        }
        return true;
    }
    // DESTROY
    delete_cookie("JSESSIONID");

</SCRIPT>

Give another look to this answer。使用 JavaScript,可以读取、修改 JSESSIONID,使其会话丢失或被劫持。

[第 3 部分]:关闭浏览器后保持会话

关闭并重新打开浏览器后,他们的帐户仍然存在 登录。 但是有些网站,不能那样做。 我很困惑它被认为是会话还是 cookie??

这是饼干。

我们看到,当 Web 浏览器删除 JSESSIONID 会话 cookie 时,服务器端的会话对象会丢失。如果没有正确的 ID,就无法再次访问它。

如果我希望我的网站以这样的方式登录,我是否必须设置 session.setMaxInactiveInterval() 还是 cookie.setMaxAge()?

我们还看到session.setMaxInactiveInterval() 是为了防止无限期地运行丢失的会话。 JSESSIONID cookie cookie.setMaxAge() 也不会让我们到达任何地方。

使用带有会话 ID 的持久性 cookie:

我在阅读了以下主题后得出了这个解决方案:

主要思想是将用户的会话注册到一个 Map 中,放入 servlet 上下文中。每次创建会话时,都会以 JSESSIONID 值为 key 将其添加到 Map 中;还会创建一个持久性cookie来记忆JSESSIONID值,以便在JSESSIONID cookie被销毁后找到会话。

当您关闭 Web 浏览器时,JSESSIONID 将被销毁。但是所有 HttpSession 对象地址都保存在服务器端的 Map 中,您可以使用保存在持久 cookie 中的值访问正确的会话。

首先,在您的 web.xml 部署描述符中添加两个侦听器。

<listener>
    <listener-class>
        fr.hbonjour.strutsapp.listeners.CustomServletContextListener
    </listener-class>
</listener>

<listener>
    <listener-class>
        fr.hbonjour.strutsapp.listeners.CustomHttpSessionListener
    </listener-class>
</listener>

CustomServletContextListener 在上下文初始化时创建一个映射。此地图将注册用户在此应用程序上创建的所有会话。

/**
 * Instanciates a HashMap for holding references to session objects, and
 * binds it to context scope.
 * Also instanciates the mock database (UserDB) and binds it to 
 * context scope.
 * @author Ben Souther; ben@souther.us
 * @since Sun May  8 18:57:10 EDT 2005
 */
public class CustomServletContextListener implements ServletContextListener{

    public void contextInitialized(ServletContextEvent event){
        ServletContext context = event.getServletContext();

        //
        // instanciate a map to store references to all the active
        // sessions and bind it to context scope.
        //
        HashMap activeUsers = new HashMap();
        context.setAttribute("activeUsers", activeUsers);
    }

    /**
     * Needed for the ServletContextListener interface.
     */
    public void contextDestroyed(ServletContextEvent event){
        // To overcome the problem with losing the session references
        // during server restarts, put code here to serialize the
        // activeUsers HashMap.  Then put code in the contextInitialized
        // method that reads and reloads it if it exists...
    }
}

CustomHttpSessionListener 在创建会话时会将其放入 activeUsers 映射中。

/**
 * Listens for session events and adds or removes references to 
 * to the context scoped HashMap accordingly.
 * @author Ben Souther; ben@souther.us
 * @since Sun May  8 18:57:10 EDT 2005
 */
public class CustomHttpSessionListener implements HttpSessionListener{

    public void init(ServletConfig config){
    }

    /**
     * Adds sessions to the context scoped HashMap when they begin.
     */
    public void sessionCreated(HttpSessionEvent event){
        HttpSession    session = event.getSession();
        ServletContext context = session.getServletContext();
        HashMap<String, HttpSession> activeUsers =  (HashMap<String, HttpSession>) context.getAttribute("activeUsers");

        activeUsers.put(session.getId(), session);
        context.setAttribute("activeUsers", activeUsers);
    }

    /**
     * Removes sessions from the context scoped HashMap when they expire
     * or are invalidated.
     */
    public void sessionDestroyed(HttpSessionEvent event){
        HttpSession    session = event.getSession();
        ServletContext context = session.getServletContext();
        HashMap<String, HttpSession> activeUsers = (HashMap<String, HttpSession>)context.getAttribute("activeUsers");
        activeUsers.remove(session.getId());
    }

}

使用基本表单通过名称/密码测试用户身份验证。此 login.jsp 表单仅用于测试。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
        <title><bean:message key="formulaire1Title" /></title>
    </head>
    <body>
        <form action="login.go" method="get">
            <input type="text" name="username" />
            <input type="password" name="password" />
            <input type="submit" />
        </form>
    </body>
</html>

我们去吧。当用户不在会话中时,此 java servlet 将转发到登录页面,并在他处于会话时转发到另一个页面。它仅用于测试持久会话!

public class Servlet2 extends AbstractServlet {

    @Override
    protected void doGet(HttpServletRequest pRequest,
            HttpServletResponse pResponse) throws IOException, ServletException {
        String username = (String) pRequest.getParameter("username");
        String password = (String) pRequest.getParameter("password");
        // Session Object
        HttpSession l_session = null;

        String l_sessionCookieId = getCookieValue(pRequest, "JSESSIONID");
        String l_persistentCookieId = getCookieValue(pRequest, "MY_SESSION_COOKIE");

        // If a session cookie has been created
        if (l_sessionCookieId != null)
        {
            // If there isn't already a persistent session cookie
            if (l_persistentCookieId == null)
            {
                addCookie(pResponse, "MY_SESSION_COOKIE", l_sessionCookieId, 1800);
            }
        }
        // If a persistent session cookie has been created
        if (l_persistentCookieId != null)
        {
            HashMap<String, HttpSession> l_activeUsers = (HashMap<String, HttpSession>) pRequest.getServletContext().getAttribute("activeUsers");
            // Get the existing session
            l_session = l_activeUsers.get(l_persistentCookieId);
        }
        // Otherwise a session has not been created
        if (l_session == null)
        {
                    // Create a new session
            l_session = pRequest.getSession();
        }

            //If the user info is in session, move forward to another page
        String forward = "/pages/displayUserInfo.jsp";

        //Get the user
        User user = (User) l_session.getAttribute("user");

        //If there's no user
        if (user == null)
        {
                    // Put the user in session
            if (username != null && password != null)
            {
                l_session.setAttribute("user", new User(username, password));
            }
                    // Ask again for proper login
            else
            {
                forward = "/pages/login.jsp";
            }
        }
        //Forward
        this.getServletContext().getRequestDispatcher(forward).forward( pRequest, pResponse );

    }

MY_SESSION_COOKIE cookie 将保存 JSESSIONID cookie 的值。当 JSESSIONID cookie 被销毁时,MY_SESSION_COOKIE 仍然存在会话 ID。

JSESSIONID 已随 Web 浏览器会话消失,但我们选择使用持久且简单的 cookie,以及放入应用程序上下文中的所有活动会话的映射。持久性 cookie 允许我们在地图中找到正确的会话。

不要忘记 BalusC 提供的这些有用的方法来添加/获取/删除 cookie:

/**
 * 
 * @author BalusC
 */
public static String getCookieValue(HttpServletRequest request, String name) {
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if (name.equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
    }
    return null;
}

/**
 * 
 * @author BalusC
 */
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
    Cookie cookie = new Cookie(name, value);
    cookie.setPath("/");
    cookie.setMaxAge(maxAge);
    response.addCookie(cookie);
}

/**
 * 
 * @author BalusC
 */
public static void removeCookie(HttpServletResponse response, String name) {
    addCookie(response, name, null, 0);
}

}

最后一个解决方案在 localhost 上使用 glassfish 进行了测试,在 windows 上使用 chrome 用于 webbrowser。它只依赖于一个cookie,你不需要数据库。但实际上,我不知道这种机制的局限性是什么。我只花了一夜的时间来解决这个问题,不知道它是好是坏。

谢谢

我还在学习中,如果我的答案有任何错误,请告诉我。谢谢,@+

【讨论】:

  • 您忘记解释/回答 “关闭并重新打开浏览器后,他们的帐户仍处于登录状态。” 部分。
  • @BalusC 你是对的。我把你关于这个主题的好答案加红了。然后我花了一个晚上的时间寻找另一个解决方案,以回答这个问题。我不知道解决方案的限制是什么,但测试起来很有趣。谢谢;-)
  • 您的解决方案有很多缺陷:(1)它从多个线程访问非同步映射,对 activeUsers 使用 ConcurrentHashMap 或 Collections.synchronizedMap,(2)对于登录检查,您应该使用过滤器,而不是 servlet .如果您通过它路由所有流量,则可以使用 servlet 来执行此操作,但 Filter 就是为此而设计的。 (3) 如果在服务器上的会话过期后(在 sessionDestroyed 方法中)删除它,为什么还要费心将会话存储到 ServletContext? (4) 如果你想要持久登录,你应该使用持久数据存储。这将无法在服务器重新启动后继续存在。等等。
  • 我很高兴有人在 2 年后审查了这段代码——因为这是我当时所要求的。使用 servlet 上下文是一个实验,但很明显数据库是存储持久数据的正确方法。我不会修改这个答案,因为已经完成了。相反,我宁愿在现实生活中测试你自己的答案并让它运行。
  • "JSESSIONID 没有过期日期:它是一个会话 cookie" 这是正确的吗?不是通过 10 控制的吗?
【解决方案2】:

正确答案有很多缺陷,请参阅我的评论。事情其实比较简单。您将需要一个持久数据存储(例如 SQL 数据库)。您也可以使用ServletContext,但用户将在服务器重新启动或应用程序重新部署后注销。如果您在ServletContext 中使用HashMap,请不要忘记正确同步,因为它可能会被更多线程同时访问。

不要破解服务器的会话及其 ID,它不在您的控制之下,如果在服务器使原始会话过期后出现带有 JSESSIONID 的请求,则某些服务器会更改会话 ID。滚动你自己的饼干。

基本上你需要:

  • 自己的 cookie,不是持久的,具有安全随机值
  • 数据存储
  • javax.servlet.Filter 检查登录

过滤器实现可能如下所示:

public class LoginFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        // Java 1.8 stream API used here
        Cookie loginCookie = Arrays.stream(req.getCookies()).filter(c -> c.getName()
                .equals("MY_SESSION_COOKIE")).findAny().orElse(null);

        // if we don't have the user already in session, check our cookie MY_SESSION_COOKIE
        if (req.getSession().getAttribute("currentUser") == null) {
            // if the cookie is not present, add it
            if (loginCookie == null) {
                loginCookie = new Cookie("MY_SESSION_COOKIE", UUID.randomUUID().toString());
                // Store that cookie only for our app. You can store it under "/", 
                // if you wish to cover all webapps on the server, but the same datastore
                // needs to be available for all webapps.
                loginCookie.setPath(req.getContextPath());
                loginCookie.setMaxAge(DAYS.toSeconds(1)); // valid for one day, choose your value
                resp.addCookie(loginCookie);
            }
            // if we have our cookie, check it
            else {
                String userId = datastore.getLoggedUserForToken(loginCookie.getValue());
                // the datastore returned null, if it does not know the token, or 
                // if the token is expired
                req.getSession().setAttribute("currentUser", userId);
            }
        }
        else {
            if (loginCookie != null)
                datastore.updateTokenLastActivity(loginCookie.getValue());
        }

        // if we still don't have the userId, forward to login
        if (req.getSession().getAttribute("currentUser") == null)
            resp.sendRedirect("login.jsp");
        // else return the requested resource
        else
            chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }

}

用户登录后,您应该将 MY_SEESSION_COOKIE 的值与 userId 一起添加到数据存储区,并在注销时将其删除。您还必须将到期日期存储到数据存储区并在接受令牌之前对其进行检查,您不得依赖于尊重 maxAge 属性的浏览器。

不要忘记添加一些数据存储清理以防止未完成的 cookie 永远存在。

上面的代码没有在现实生活中测试过,可能会有一些怪癖,但基本的想法应该是可行的。它至少比公认的解决方案好很多。

【讨论】:

  • 我是 servlet 的新手,经过长时间的 SO 和 Google 搜索,我发现您的答案最容易“实施”,没有严重的安全问题。应该赞成。
  • 好方法!如果我的 Spring Security 配置有“maximumSessions=1”和“exceptionIfMaximumExceeded=true”,我需要做什么才能达到相同的结果?
猜你喜欢
  • 2021-09-21
  • 2012-02-10
  • 1970-01-01
  • 2010-11-05
  • 1970-01-01
  • 1970-01-01
  • 2015-11-25
  • 2013-09-09
  • 2011-07-31
相关资源
最近更新 更多