如何在生产中安全地更改会话 cookie 域或名称?

How to safely change the session cookie DOMAIN or name in production?

我们最近意识到我们的会话 cookie 被写到我们网站的完全限定域名 www.myapp.com,例如:

MYAPPCOOKIE: 79D5DB83..., domain: www.myapp.com

我们希望将其切换为可以跨子域共享的 cookie,因此 myapp.com 中的任何服务器也可以使用会话 cookie。例如,我们希望我们的 cookie 像这样存储:

MYAPPCOOKIE: 79D5DB83..., domain: .myapp.com

我们试过只更改我们的会话 cookie 以像这样使用该域:

   Cookie sessionCookie = sessionManager.getSessionIdCookie();
   sessionCookie.setDomain(".myapp.com");                

这在 大多数 情况下工作正常。

我们发现在某些情况下,一些用户在部署此新代码后无法登录。当用户出现问题:

他们的浏览器中似乎有 2 个会话 cookie:

管理这个旧 cookie 的最佳方法是什么?如果用户没有会话,我们已经尝试添加一些代码来删除旧的 cookie,但是在我们应用程序的某些路径中这似乎不起作用。

如果可行,我们愿意重命名 cookie,并且正在寻找其他人可能提出的任何建议。谢谢!

以下方法解决了这个问题,方法是将 session cookie 重命名为新名称,如果需要,将旧值复制到新的 cookie 名称。

它使用一些 Apache 模块 (mod_headers and mod_setenvif) 在新 cookie 不存在时将旧 cookie 值复制到新 cookie 名称。

  # We only want to copy the old cookie to the new cookie name
  # when it doesnt already exist in the request. To do so, we
  # set a "per request" flag which is used to determine if we need
  # to do the "cookie copy operation".

  <IfModule mod_setenvif.c>
    SetEnvIf Cookie "NEW_SESSION_ID=" HAS_NEW_SESSION_ID
  </IfModule mod_setenvif.c>

  # If the cookie "NEW_SESSION_ID" doesnt exist in the request, copy 
  # the old cookie value to the new cookie name. This allows seamless 
  # switching to the new cookie for users with existing sessions, and
  # also ensure that if we have to rollback our code change for some 
  # reason, the old cookie will still be ok. Of course if new sessions 
  # are created with the new cookie, then they wouldn't be copied over on
  # rollback, but a similar approach could be used if someone 
  # wanted to do so

  <IfModule mod_headers.c>
     RequestHeader edit Cookie "OLD_SESSION_ID=([0-9a-zA-Z\-]*)" "NEW_SESSION_ID= DEBUG_MSG_FOR_REPLACING=TRUE; OLD_SESSION_ID=" env=!HAS_NEW_SESSION_ID
  </IfModule mod_headers.c>

请注意,您可以通过查找 DEBUG_MSG_FOR_REPLACING cookie 值来测试替换是否有效,我添加该值是为了在替换完成后进行调试。

以下是端点的一些示例代码,它只是将 cookie header 的值转储到响应中,您可以在调试 Apache 更改时使用它:

@GET
@Path("dump_cookies")
public Object dumpCookies(@Context HttpServletRequest request)
{
  String result = request.getHeader("Cookie");
  String cookies = result.replace("; ", "<br/>");

  String html = "<h1>Raw</h1>" + result;

  html += "<hr/>";
  html += "<h1>Cookies</h1>" + cookies;

  return html;
}

请注意,由于业务原因阻止我们重命名 cookie,我们最终并未使用此解决方案。我们最终使用了 this solution instead,这使我们能够保持我们的 cookie 名称相同。

如果您使用的是 Apache Shiro,此方法有效,并在创建新 cookie 之前删除具有完全限定域名的旧 cookie。

ShiroSessionManager.java

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

public class ShiroSessionManager extends DefaultWebSessionManager
{
    @Override
    protected void onStart(Session session, SessionContext context)
    {
        if (isSessionIdCookieEnabled())
        {
            HttpServletRequest request = WebUtils.getHttpRequest(context);
            HttpServletResponse response = WebUtils.getHttpResponse(context);

            removeSessionCookieForFullDomain(request, response);
        }

        super.onStart(session, context);
    }

    private void removeSessionCookieForFullDomain(HttpServletRequest request, HttpServletResponse response)
    {
        Cookie sessionCookie = getSessionIdCookie();
        Cookie cookie = new SimpleCookie(sessionCookie.getName());

        cookie.setSecure(true);
        cookie.setHttpOnly(true);
        cookie.setComment(sessionCookie.getComment());

        // Setting the domain to null means use the fully qualified domain name
        cookie.setDomain(null);

        cookie.setMaxAge(sessionCookie.getMaxAge());
        cookie.setPath(sessionCookie.getPath());
        cookie.setValue(sessionCookie.getValue());
        cookie.setVersion(sessionCookie.getVersion());

        cookie.removeFrom(request, response);
    }
}

要使用它,您需要在安全管理器上设置会话管理器,例如:

    // Create our shiro environment
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    DefaultWebEnvironment environment = new DefaultWebEnvironment();
    DefaultWebSessionManager sessionManager = new ShiroSessionManager();

    // Use the new session manager
    securityManager.setSessionManager(sessionManager);
    environment.setWebSecurityManager(securityManager);
    SecurityUtils.setSecurityManager(securityManager);