Session的管理和并发
Spring Security的一个常见配置就是检测相同的用户以不同的session登录安全系统。这被称为并发控制(concurrency control),是session管理(session management)一系列相关配置功能的一部分。严格来说,这个功能并不是高级配置,但是它会让很多新手感到迷惑,并且最好在你对Sping Security整体功能有所了解的基础上再掌握它。Spring Security的session管理能够以两种不同的方式进行配置——session固化保护(session fixation protection)和并发控制。因为并发控制的功能基于session固化保护所提供的框架,我们先介绍session固化。
配置session fixation防护
如果我们使用的是security命名空间的配置方式,session固化防护已经被默认进行了配置。如果我们要指明将其配置为与默认设置一致的话,我们需要这样:
- <http auto-config="true" use-expressions="true">
- <!-- ... -->
- <session-management session-fixation-protection="migrateSession"/>
- </http>
Session固化防护这个功能你可能并不会在意,除非你想扮演一个恶意的用户。我们将向你展示如何模拟一个session窃取攻击,但是在此之前,有必要理解session固化是怎么回事以及怎样防止这样的攻击。
理解session fixation攻击
Session固化是恶意用户试图窃取系统中一个未认证用户的session。对攻击者来说,可以通过各种技术来获取用户session的唯一标识(例如,JSESSIONID)。如果攻击者创建了带有用户JSESSIONID的cookie或者URL参数,他就能够访问用户的session。
尽管这是一个明显的问题,但是一般情况下,如果用户没有经过认证,他们就还没有输入任何敏感信息(假设站点的安全已经正确规划了)。如果用户认证后依旧使用相同的session标识符,这个问题就会比较更加重要了。如果用户在认证后还使用相同的标识符,那攻击者现在就能访问认证过用户的session,而甚至不必要知道他们的用户名和密码。
【此时,你可能很不屑并认为在现实世界中这不会发生。实际上,session窃取攻击经常发生。关于这个话题,我们建议(正如在第三章那样)你花些时间阅读由OWASP组织(http://www.owasp.org/)发布的包含重要信息的文章以及学习案例。攻击者和恶意用户是真实存在的,如果你不了解他们常用的技术及如何避免,他们会对你的用户、应用或公司造成真正的损害。】
下图展现了session固化攻击是如何发生的:
既然了解了攻击如何进行,接下来我们查看Spring Security如何防止。
使用Spring Security防止session fixation攻击
如果我们能够阻止用户在认证前和认证后使用相同的session,我们就能够让攻击者掌握的session ID信息变得没有用处。Spring Security的session固化防护解决这个问题的方式就是在用户认证之后明确创建一个新的session并将旧的session失效。
让我们看下图:
我们可以看到一个新的过滤器,o.s.s.web.session.SessionManagementFilter,负责检查一个特定的用户是否为新认证的。如果用户是新认证的,一个配置的o.s.s.web.authentication.session.SessionAuthenticationStrategy将确定要怎样做。o.s.s.web.authentication.session.SessionAuthenticationStrategy将会创建一个新的session(如果用户已经拥有一个的话),并将已存在session的内容拷贝到新session中去。这看起来很简单,但是,通过上面的图表我们可以看到,它能够有效组织恶意用户在未知用户登录后重用session ID。
模拟session fixation攻击
此时,你可能会想要看一下在模拟session固化攻击时会涉及到什么。为了实现这一点,你需要在dogstore-security.xml中配置session固化防护失效。
接下来,你需要打开两个浏览器。我们将会在IE中初始化session,并从那里窃取,我们的攻击者将会使用窃取到的session在Firefox中登录。我们将会使用Internet Explorer Developer Tools (IE 8中自带)以及Firefox Web Developer Add-On(第三章中已经给过URL)来查看和控制cookie。
在IE中打开JBCP Pets首页,然后打开开发者工具(看“工具”下拉菜单或点击F12),并在“缓存”菜单下选择“查看Cookie信息”。在合适域下(如果使用localhost将为空)找到JSESSIONID的cookie。
将session cookie的值复制到粘贴板上,然后登录JBCP Pets站点。如果你重复“查看Cookie信息”,你将会发现JSESSIONID在登录后没有变化,这将会导致很容易受到session固化攻击。
在Firefox下,打开JBCP Pets站点。你将会被分配一个session cookie,这能通过Cookie菜单的“查看Cookie信息”菜单项查看到。
为了完成我们的攻击,我们点击“Edit Cookie”选项,并将从IE中复制到粘贴板上的JSESSIONID值粘贴进来,如下图所示:
我们的session固化攻击完成了!如果此时在Firefox中重新加载页面,你将以IE中已登录用户相同的身份进入系统,并不需要你知道用户名和密码。你是否害怕恶意用户了?
现在,重新使session固化防护生效然后重新尝试这个练习。你会发现,在这种情况下,JSESSIONID在用户登录后会发生变化。因为你已经了解了session固化攻击是如何发生的,这意味着减少了可信任用户陷入这种攻击的风险。干的漂亮!
细心的开发人员应该会注意到有很多种窃取session cookie的方式,有一些如跨站脚本攻击(XSS)可能会使得session固化防护都很脆弱。请访问OWASP站点来了解更多防止这种类型攻击的信息。
比较session-fixation-protection选项
session-fixation-protection属性支持三种不同的选项允许你进行修改:
属性值 | 描述 |
none | 使得session固化攻击失效,不会配置SessionManagementFilter(除非其它的<session-management>属性不是默认值) |
migrateSession | 当用户经过认证后分配一个新的session,它保证原session的所有属性移到新session中。我们将在后面的章节中讲解,通过基于bean的方式如何进行这样的配置。 |
newSession | 当用户认证后,建立一个新的session,原(未认证时)session的属性不会进行移到新session中来。 |
在大多数场景下,默认行为即migrateSession适用于在用户登录后希望保持重要信息(如点击爱好、购物车等)的站点的站点
通过session的并发控制增强对用户的保护
紧随session固化防护一个很自然的用户安全增强功能就是session并发控制。如前面所描述的那样,session并发控制能够确保一个用户不能同时拥有超过一个固定数量的活跃session(典型情况是一个)。要确保这个最大值的限制需要涉及到好几个组件的协作以精确跟踪用户session活动的变化。
让我们配置这个功能并了解其如何工作,然后对其进行测试。
配置session并发控制
既然我们要了解session并发控制所要涉及的组件,那将其运行环境搭建起来可能会更有感官的了解。首先,我们需要使得ConcurrentSessionFilter生效并在dogstore-security.xml配置。
- <http auto-config="true" use-expressions="true">
- <!-- ... -->
- <session-management>
- <concurrency-control max-sessions="1"/>
- </session-management>
- </http>
现在,我们需要在web.xml描述文件中配置中使得o.s.s.web.session.HttpSessionEventPublisher生效,这样servelt容器将会通知Spring Security session生命周期的事件(通过HttpSessionEventPublisher)。
- <listener>
- <listener-class>
- org.springframework.web.context.ContextLoaderListener
- </listener-class>
- </listener>
- <listener>
- <listener-class>
- org.springframework.security.web.session
- .HttpSessionEventPublisher
- </listener-class>
- </listener>
- <servlet>
- <servlet-name>dogstore</servlet-name>
这两个配置完成,session的并发控制功能也就激活了。让我们看一下它内部是如何工作的,然后我们将会通过几步操作来查看它对用户的session的保护功能。
理解session并发控制
我们在前面提到session并发控制试图限制相同的用户以不同的session进行访问。基于我们对session窃取方式攻击的了解,我们可以发现session并发控制能够降低攻击者窃取已登录合法用户session的风险。你觉得为什么会这样呢?
Session并发控制使用o.s.s.core.session.SessionRegistry来维护一个活跃HTTP session的列表而认证过的用户与其进行关联。当session创建或过期时,注册表中会实时进行更新,基于HttpSessionEventPublisher发布的session生命周期事件来跟踪每一个认证用户的活动session的数量。
SessionAuthenticationStrategy的一个扩展类即o.s.s.web.authentication.session.ConcurrentSessionControlStrategy提供方法来实现新session的跟踪以及session并发控制的实际增强功能。每次用户访问这个安全站点时,SessionManagementFilter将会比照SessionRegistry检查这个活跃的session。如果用户活跃的session不在SessionRegistry这个活跃session列表中,最近最少被使用的session将会立即过期。
在修改后的session并发控制过滤器链中的第二个参与者是o.s.s.web.session.ConcurrentSessionFilter。这个过滤器能够辨认出过期的session(典型情况下,session会被servlet容器或者被ConcurrentSessionControlStrategy强制失效掉)并通知用户他的session已经失效了。
既然我们已经了解了session并发控制是如何工作的,那对我们来说很容易制造一个它使用的场景。
测试session并发控制
如同验证session固化攻击那样,我们需要访问两个web浏览器。按一下的步骤:
1. 在IE中,以guest用户登录;
2. 接下来,在Firefox中,以相同的用户(guest)登录;
3. 最后,返回到IE中,做任何的动作都可以。你会发现有一个信息提示你的session已经过期了。
将会显示以下的信息:
尽管不很友好,但是它能够表明session已经被软件强制失效了。如果你是一个攻击者,现在你可能会很灰心。但是,如果你是一个合法的用户,你可能会很迷惑,因为这显然不是一个友好的方式来表明JBCP Pets一直关注着你的安全。
【session并发控制对Spring Security的新用户来说是很难掌握的概念。很多用户在还没有完全理解其怎样运行和能带来什么好处时就尝试实现它。如果你想使用这个强大的功能,并且它不像你预想的那样工作,请确保你的配置全部正确并回顾一下本节讲的原理——希望它能帮助你理解什么出错了。】
在这样的事件发生时,我们应该将用户重定向到登录页,并提供一个信息来说明发生了什么错误。
配置session失效时的重定向地址
幸运的是,有一种很容易的方法使用户在session并发控制后重定向到友好的页面(一般来说,是登录页),即设置expired-url属性为一个你应用中合法的页面。
- <http auto-config="true" use-expressions="true">
- <!-- ... -->
- <session-management>
- <concurrency-control max-sessions="1" expired-url= "/login.do?error=expired"/>
- </session-management>
- </http>
这样在我们的应用中,就会将用户重定向到登录form,并且我们可以修改这个页面来展现用户友好的信息来表明发现了多个活跃的session,从而需要重新登录。当然,这个URL是完全随意的,并根据你应用的需求来进行相应的调整。
Session并发控制的其它好处
Session并发控制的另一个好处是存在SessionRegistry跟踪活跃的session(过期session是可选的)。这意味着我们能够得到系统中运行时的用户活动信息(至少是认证过的用户)。
即使你不想使用session并发控制,你可以可以这样做。只需将max-sessions的值设置为-1,这样session跟踪会保持可用,但没有最大session个数的限制。
让我们看两个使用此功能的两种简单方式。
显示活动的用户
你可能会在线论坛上见到显示系统中当前活跃用户的数量。借助于使用session注册跟踪(通过session并发控制),很容易实现在应用中的每个页面对此进行展现。
让我们在BaseController中添加一个简单的方法以及bean自动织入。@Autowired
- SessionRegistry sessionRegistry;
- odelAttribute("numUsers")
- public int getNumberOfUsers() {
- return sessionRegistry.getAllPrincipals().size();
- }
我们可以看到这暴露了一个能够在Spring MVC JSP页面中能够使用的属性,所以我们添加一个页脚footer.jsp到JBCP Pets站点中并使用这个属性。
如果你重新启动应用并登录,能够在每个页面的底部看到活动用户的数量。
很简单,但是它阐述了作为Spring Security一部分所提供的session跟踪的好处——尤其是你能够直接使用这个内置的功能。
我们能够借助于SessionRegistry进行更高级的收据收集,这对于管理员来说很有用——现在让我们看一下。
显示所有用户的信息
SessionRegistry跟踪所有活跃用户session的信息。如果你想增强站点的管理,我们可以可以很容易地在一个页面中列出所有的用户活跃用户以及他们在站点中使用的名字。
让我们为AccountController添加一个新的方法(尽管这样的功能一般会添加在在管理区域,但是此时我们可以假设JBCP Pets并不是一个真正的站点),这个方法将会查找SessionRegistry中的信息并收集当前session的信息。
- @RequestMapping("/account/listActiveUsers.do")
- public void listActiveUsers(Model model) {
- Map<Object,Date> lastActivityDates = new HashMap<Object, Date>();
- for(Object principal: sessionRegistry.getAllPrincipals()) {
- // a principal may have multiple active sessions
- for(SessionInformation session : sessionRegistry.getAllSessions(principal, false))
- {
- // no last activity stored
- if(lastActivityDates.get(principal) == null) {
- lastActivityDates.put(principal, session.getLastRequest());
- } else {
- // check to see if this session is newer than the last stored
- Date prevLastRequest = lastActivityDates.get(principal);
- if(session.getLastRequest().after(prevLastRequest)) {
- // update if so
- lastActivityDates.put(principal, session.getLastRequest());
- }
- }
- }
- }
- model.addAttribute("activeUsers", lastActivityDates);
- }
这个方法使用了SessionRegistry的两个API。
l getAllPrincipals:返回拥有活跃session的Principal对象(典型情况下为UserDetails对象)所组成的List;
l getAllSessions(principal, includeExpired):得到指定Principal的SessionInformation组成的List,包含了每个session的信息。也能够包含过期的session。
了解了SessionRegistry API方法,listActiveUsers方法的逻辑就很简单了——检索注册表中所有的活跃用户并寻找最近活动的session。Principal以及最近活跃的时间戳信息插入到一个Map中用来在UI中展现。
UI页面通过使用JSTL结构变得很简单了。在WEB-INF/views/account目录下,创建listActiveUsers.jsp,内容如下(简便起见,我们省略了头部和尾部信息):
- <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
- <h1>Active Users</h1>
- <ul>
- <c:forEach items="${activeUsers}" var="uinfo">
- <li><strong>${uinfo.key.username}</strong>
- / Last Active: <strong>${uinfo.value}</strong></li>
- </c:forEach>
- </ul>
最后,当我们导航到http://localhost:8080/JBCPPets/account/listActiveUsers.do,页面基本如下:
通过这些例子,你已经了解到了SessionRegistry的威力。我们甚至可以扩展SessionRegistry来跟踪用户活动的附加信息,如最后访问的页面、最后的行为等——这对基于Spring Security构建管理界面来说是很有用的。