在浏览器标签页中如何区分会话?

143

在使用JSP和Servlets实现的Java Web应用程序中,如果我将信息存储在用户会话中,则所有来自同一浏览器标签页的信息都是共享的。如何区分浏览器标签页中的会话? 在此示例中:

<%@page language="java"%>
<%
String user = request.getParameter("user");
user = (user == null ? (String)session.getAttribute("SESSIONS_USER") : user);
session.setAttribute("SESSIONS_USER",user);
%>
<html><head></head><body>
<%=user %>
<form method="post">
User:<input name="user" value="">
<input type="submit" value="send">
</form>
</body></html>

将以下代码复制到JSP页面(testpage.jsp),将该文件部署在服务器上现有Web应用程序的上下文中(我使用Apache Tomcat),然后使用正确的URL(localhost/context1/testpage.jsp)在浏览器(FF、IE7或Opera)中打开,输入您的名字并提交表单。接着在同一浏览器中打开一个新标签页,您就可以看到您的名字(从会话中获取)。请注意浏览器缓存,有时它似乎没有发生变化,但实际上已被缓存在其中,请刷新第二个选项卡。

谢谢。


相关内容:https://dev59.com/a2855IYBdhLWcg3wMRVW#4480310 - Bozho
1
这是用户必须执行的操作:打开IE,单击“文件->新建会话”。 - Stefan Steiger
3
@Quandary,你的解决方案不是通用的(在其他浏览器上不起作用),而且最重要的是不够用户友好(用户不了解会话)。 - Oriol Terradas
2
有些人似乎无法想象这个的目的是什么。问题领域是任何你想允许不同“视图”你的网站的情况。一旦用户可以拥有多个视图,他们不可避免地会渴望(或者意外尝试)同时访问两个不同的视图。例如:时间版本控制(切换到查看网站在过去某个时间点存在的状态);沙盒(对其他人不可见的更改网站);基于角色的视图(查看网站如何呈现给权限较低的用户)等。 - Ron Burk
截至今日,如果选项卡位于不同的浏览器窗口中,现在有一个简单的解决方案,因为一些浏览器现在支持配置文件(例如,参见 https://www.wired.com/story/how-to-use-browser-profiles-organization-chrome-edge-firefox/)。 - Marco
20个回答

100
你可以使用 HTML5 SessionStorage (window.sessionStorage)。您将生成一个随机 ID 并将其保存在每个浏览器标签的会话存储中。然后,每个浏览器选项卡都有自己的 ID。

使用 sessionStorage 存储的数据不会跨浏览器选项卡持久化,即使两个选项卡都包含来自相同域起源的网页。换句话说,sessionStorage 中的数据仅限于调用页面的域和目录,以及包含页面的浏览器选项卡。与此相反的是,会话 cookie 可以将数据从选项卡保存到另一个选项卡。


7
从文档中得知:使用sessionStorage存储的数据无法跨浏览器标签保留,即使两个标签都包含来自同一个域的网页。换句话说,sessionStorage内的数据仅限于调用页面的域和目录,以及包含页面的浏览器标签。与此相比,会话cookie可以在标签之间保留数据。在Chrome和FF简单测试可以确认这种行为。 - jswanson
24
注意,在使用谷歌浏览器的“复制标签页”功能时,ID将会重复。通常这不会成为问题,但应该仔细考虑。 - JosiahDaniels
@StefanSteiger。然后您可以使用Ajax调用发送BrowserTab id(保存在Session Storage中)。在服务器端,您将需要自定义逻辑,因为WebSession是相同的。但是,您可以通过选项卡创建一个包含会话对象的HashMap。 - Gonzalo Gallotti
@Gonzalo Gallotti:你的ajax调用在表单提交(postback)哪里?此外,我知道这一点。我的观点是,如果你这样做,你将不得不对使用会话(session)的每个字节代码进行大量更改。数百页,潜在地成千上万的文件。也许还有其他项目中使用的库。你永远不会这样做。即使对于非常小的项目,这也是一个选项。但是,如果项目如此之小,我宁愿完全消除对任何会话对象的依赖,因为通常这是反模式(即非常非常非常糟糕的事情)。 - Stefan Steiger
@Gonzalo Gallotti:使用通配符DNS条目的多个子域名很可能是一个更好的选择,因为这只需要更改登录/SAML代码。https://dev59.com/InRC5IYBdhLWcg3wOeaB#23075956 - Stefan Steiger
显示剩余3条评论

25
你必须认识到,服务器端会话是HTTP的一种人工附加功能。由于HTTP是无状态的,服务器需要以某种方式识别请求属于其已知并具有会话的特定用户。有两种方法可以做到这一点:
  • Cookies。更干净和流行的方法,但这意味着一个用户的所有浏览器选项卡和窗口共享会话 - 在我看来,这实际上是可取的,如果一个网站让我为每个新标签登录,我会非常恼火,因为我非常频繁地使用选项卡。
  • URL 重写。站点上的任何URL都附加了会话ID。这更需要工作量(您必须在站点内链接处执行某些操作),但可以在不同的选项卡中拥有单独的会话,尽管通过链接打开的选项卡仍将共享会话。它还意味着用户每次进入您的网站都必须登录。
你到底想做什么?为什么要让选项卡具有单独的会话?也许有一种方法可以在根本不使用会话的情况下实现你的目标?
编辑:为测试,可以找到其他解决方案(例如在分离的VM上运行多个浏览器实例)。如果一个用户需要同时扮演不同的角色,则应该在应用程序中处理“角色”概念,以便一个登录可以具有多个角色。您必须决定哪种方式更可接受 - 使用URL重写、仅适应当前情况还是忍受当前情况,因为使用基于cookie的会话无法单独处理浏览器选项卡。

6
这是一个用于存储用户信息和环境信息的大型应用程序。当有人在不同的标签页中使用不同的用户身份登录,进行测试或扮演不同的角色时,会在这些标签页之间交换信息。请注意,此翻译中尽可能保留了原文的意思和结构,并力求通俗易懂。 - Oriol Terradas
只是一个非常晚的补充,原因是我需要知道这个信息,以便知道用户关注的是我的网站上的哪个页面,而不仅仅是他们从一个页面到另一个页面(而是从一个标签到另一个标签)。 - Oliver Williams

16

窗口名称(window.name)是JavaScript属性中唯一能在选项卡活动期间保留的内容,而且可以独立存在(而不是URL混淆码)。


3
window.sessionStorage也可以。 - felickz
3
当用户选择“在新选项卡中打开”时,需要小心处理会话存储,因为它会被复制到一个新的选项卡中...我希望有详细信息的链接,但请参见:https://bugzilla.mozilla.org/show_bug.cgi?id=818389 - felickz
2
请注意,您在window.name设置中保存的内容仍将在其他域中可用,当用户在同一标签页中切换到其他页面时。 - nakib

15

你不应该这样做。如果你想要做这种事情,你需要通过动态生成URL并使用类似sessionID(不是sessionid,它不起作用)的标识符来强制用户使用你的应用程序的单个实例,并将其传递到每个URL上。

我不知道你为什么需要这样做,但除非你想制作一个完全无用的应用程序,请不要这样做。


10
这是一个管理用户信息和环境的大型应用程序。当有人在不同的标签页中使用不同的用户身份进行登录(无论是为了测试还是为了不同的角色),那么他就会在这些标签页之间共享信息。请注意,这里的“crossing information”指的是信息互通或交叉传递,并非数据混淆或错误。 - Oriol Terradas
46
我知道这个回答旧了,但 Google 邮箱允许你在不同的标签页中使用不同的帐户,并且它并不是一个 "完全不能使用的应用程序"。 - George
7
不确定答案是否仍然正确,你可以明显地做到。 Gmail可以做到这一点。至于它如何做到,可能不够优雅,但它有效,并且这才是最重要的。 - George
2
我不同意“完全无法使用的应用程序”这部分。假设您有一个登录,然后查看应用程序中的不同帐户。您不想每次在每个帐户上执行操作时都传递帐户ID参数。因此,您将当前选择的帐户存储在会话中,在需要时获取它。然后,您还可以打开不同的选项卡以同时查看不同的帐户。在这种情况下,您在会话中拥有最新的帐户ID,但是如果您返回到另一个先前的选项卡,则在浏览器上看到的帐户与服务器上的帐户不同。 - serdar.sanri
2
旧的回答,但我可以补充说,WhatsApp和Telegram不允许您同时打开两个选项卡。这并不意味着它们无法使用。 - Bruno Francisco
显示剩余5条评论

13

我想到了一个新的解决方案,它有一点点额外的开销,但是作为原型似乎已经有效。一个假设是在登录时你处于诚信系统环境中,虽然这可以通过每次切换选项卡重新请求密码来适应。

使用localStorage(或等价物)和HTML5存储事件来检测一个新的浏览器选项卡是否切换了活动用户。当发生这种情况时,创建一个带有消息的Ghost覆盖层,说明您不能使用当前窗口(或以其他方式暂时禁用窗口,您可能不希望它过于显眼)。当窗口重新获得焦点时,发送一个AJAX请求来记录用户重新登录。

这种方法有一个警告:您不能在没有焦点的窗口中执行任何正常的AJAX调用(例如,如果您在延迟之后进行调用),除非您在此之前手动进行AJAX重新登录调用。因此,您实际上只需要让您的AJAX函数首先检查localStorage.currently_logged_in_user_id === window.yourAppNameSpace.user_id,如果不是,则通过AJAX进行登录。

另一个问题是竞争条件:如果您能够快速切换窗口以混淆它,您可能会出现重新登录1->重新登录2->ajax1->ajax2序列,其中ajax1是在错误的会话下进行的。通过将登录AJAX请求推送到数组中,并在存储之前和发出新的登录请求之前中止所有当前请求来解决此问题。

需要注意的最后一点是窗口刷新问题。如果某人在您进行 AJAX 登录请求时刷新了窗口,但该请求尚未完成,则会以错误的身份进行刷新。在这种情况下,您可以使用非标准的 beforeunload 事件向用户警告可能发生混淆的情况,并要求他们点击“取消”,同时重新发出 AJAX 登录请求。然后,他们唯一能够搞砸它的方式就是在请求完成之前单击“确定”(或意外地按下回车键/空格键,因为“确定”是默认选项,在这种情况下不幸地成为了默认选项)。还有其他处理此情况的方法,比如检测 F5 和 Ctrl+R/Alt+R 的按键,这在大多数情况下都有效,但可能会受到用户键盘快捷键重新配置或使用其他操作系统的威胁。但实际上,这只是一个边缘案例,最坏的情况也并不那么糟糕:在诚信系统配置中,您将以错误的身份登录(但是您可以通过个性化页面颜色、样式、突出显示的名称等来明确指出这一点);在密码配置中,责任在于最后一个输入密码的人是否已注销或共享其会话,或者如果这个人实际上是当前用户,则不存在违规行为。
但总之,您有一个每个选项卡只能有一个用户的应用程序(希望)可以正常运行,而无需必须设置配置文件、使用 IE 或重写 URL。但是,请确保在每个选项卡中清楚地显示谁登录了该特定选项卡...

6
因为你花时间仔细思考了,所以我给你点赞!:-) 从所有的警告中可以看出这是个坏主意。我从中学到的教训是真正理解哪些数据应该放进会话中,哪些数据不应该放进去。 - Peter

10

说实话…上面的内容可能是真的,也可能不是真的,但它所有的东西都似乎太复杂或者没有解决如何知道服务器端使用哪个选项卡。

有时候我们需要使用奥卡姆剃刀原则。

以下是奥卡姆剃刀原则的做法:(不,我不是奥卡姆,他在1347年去世了)

  1. 在页面加载时,为浏览器分配唯一的id。只有在窗口尚未拥有id时才这样做(因此使用前缀和检测)

  2. 在每个页面上(使用全局文件或其他方法),只需放置代码来检测焦点事件和/或mouseover事件。(为了方便编写代码,我将使用jquery)

  3. 在您的焦点(和/或mouseover)函数中,使用window.name设置一个cookie。

  4. 在需要读取/写入特定选项卡数据时,从服务器端读取该cookie值。

客户端:

//Events 
$(window).ready(function() {generateWindowID()});
$(window).focus(function() {setAppId()});
$(window).mouseover(function() {setAppId()});


function generateWindowID()
{
    //first see if the name is already set, if not, set it.
    if (se_appframe().name.indexOf("SEAppId") == -1){
            "window.name = 'SEAppId' + (new Date()).getTime()
    }
    setAppId()
}

function setAppId()
{
    //generate the cookie
    strCookie = 'seAppId=' + se_appframe().name + ';';
    strCookie += ' path=/';

    if (window.location.protocol.toLowerCase() == 'https:'){
        strCookie += ' secure;';
    }

    document.cookie = strCookie;
}

服务器端(以C#为例)

//variable name 
string varname = "";
HttpCookie aCookie = Request.Cookies["seAppId"];
if(aCookie != null) {
     varname  = Request.Cookies["seAppId"].Value + "_";
}
varname += "_mySessionVariable";

//write session data 
Session[varname] = "ABC123";

//readsession data 
String myVariable = Session[varname];

完成。


1
我真的很喜欢你简化的答案和奥卡姆剃刀的参考。只是想确认一下这个解决方案对你仍然有效。 - Jeff Marino
1
值得注意的是,如果刷新页面应该保留其会话,则此方法将无法工作。另一个答案中建议使用window.sessionStorage的方法对我来说更合理。 - rosenfeld
@quijibo 在我的应用程序中仍然完美运行。至于刷新页面,对于某些浏览器可能是真的,使用window.sessionStorage可能会解决这个问题,我还没有尝试过。 - Mike A.
3
"se_appframe()" 函数的作用是什么? - André Miranda
se_appframe 是什么? - Kiquenet
se_appframe()是我拥有的一个函数,它可以遍历框架(iframes、父级等)并找到我的应用程序的最顶层框架。这是一个拥有框架集的老兽。我们正在将这个老兽重写为SPA,但现在我们还被困在框架集中。在大多数情况下,您只需要寻找类似window.topmost的东西。 - Mike A.

9
我们曾遇到这个问题,但我们很容易地解决了它,而且无需编程。我们想要做的是让用户能够在同一个浏览器窗口中登录多个帐户,而不会发生会话冲突。解决方案就是使用随机子域名。
23423.abc.com
242234.abc.com
235643.abc.com

所以我们要求系统管理员为*.abc.com而不是abc.com配置SSL证书。然后进行了少量代码更改,每次用户尝试登录时,他会被登录到具有随机子域名编号的选项卡中。因此,每个选项卡都可以独立地拥有自己的会话。为了避免任何冲突,我们使用用户ID的哈希或MD5开发了随机数。


3
这似乎是一种聪明的URL重写替代方案。您最终会在HTTP符合标准但不显眼的位置看到所需的变量(会话ID)。脑海中出现的小问题:1)需要DNS通配符,显然,2)整个网站必须避免任何绝对URL,这些URL会将用户路由回(例如)“www”子域,3)可能希望让网络爬虫保持距离,但这可能是一个私有网络情况,因此已经完成了。谢谢分享! - Ron Burk
@user3534653:我喜欢这种方法。但是你说“不涉及编程”。然后你又说“只需进行少量代码更改”。那么,少量的代码更改不算编程吗?;) 此外,如果您有多个具有规范链接的应用程序,并且域名已被硬编码/配置(规范化),则此方法可能无法正常工作。 - Stefan Steiger
你可能已经想到了,但是...有什么能阻止别人猜测子域名呢?希望这不仅仅是一个随机的子域名。只需要几秒钟就可以找到所有活动的子域名,然后查看页面(除非有其他代码来防止这种情况发生)。这不是有点像会话“劫持”,但在这种情况下,它将是子域名“黑客”? :) 我相信你有更多的代码来保护它...不能依赖匿名性来保证安全。 - PerryCS

2
注意:此处的解决方案需要在应用程序设计阶段完成。稍后再进行工程化将会很困难。
使用隐藏字段来传递会话标识符。
为了使其工作,每个页面都必须包含一个表单:
<form method="post" action="/handler">

  <input type="hidden" name="sessionId" value="123456890123456890ABCDEF01" />
  <input type="hidden" name="action" value="" />

</form>

您好!这句话的意思是:您在网页上的每一个行为,包括导航和提交表单(设置适当的“action”),都会将表单重新提交。对于"unsafe"请求,您可以添加另一个参数,比如包含要提交的数据的JSON值。
<input type="hidden" name="action" value="completeCheckout" />
<input type="hidden" name="data" value='{ "cardNumber" : "4111111111111111", ... ' />

由于没有cookies,每个选项卡都是独立的,不会知道同一浏览器中的其他会话。
有很多优点,特别是在安全方面:
  • 不依赖JavaScript或HTML5。
  • 本质上可以防止 CSRF攻击。
  • 不依赖cookies,因此可以防止 POODLE攻击。
  • 不容易受到 会话固定攻击。
  • 可以防止使用后退按钮,这在您希望用户遵循站点上的一组路径时非常有用(这意味着有时可以通过无序请求攻击的逻辑错误可以被防止)。

一些缺点:

  • 可能需要后退按钮功能。
  • 与缓存一起使用效果不太好,因为每个操作都是POST。

此处有更多信息


2

在从单个页面(例如index.html/jsp/whatever)开始时,您可以使用链接重写将唯一标识符附加到所有URL上。浏览器将为所有选项卡使用相同的cookie,因此您放入cookie中的所有内容都不会是唯一的。


2
我认为重写链接以编码会话是一项繁琐且容易出错的工作。我的应用程序很大,有很多链接,我需要一个透明的解决方案或易于实现的方案。 - Oriol Terradas

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接