只需在两个浏览器中用同一个账号登录就会发现,到目前为止,系统尚未有任何会话并发限制。一个账户能多处同时登录可不是一个好的策略。事实上,Spring Security已经为我们提供了完善的会话管理功能,包括会话固定攻击、会话超时检测以及会话并发控制。
理解会话
会话(session)就是无状态的 HTTP 实现用户状态可维持的一种解决方案。HTTP 本身的无状态使得用户在与服务器的交互过程中,每个请求之间都没有关联性。这意味着用户的访问没有身份记录,站点也无法为用户提供个性化的服务。session的诞生解决了这个难题,服务器通过与用户约定每个请求都携带一个id类的信息,从而让不同请求之间有了关联,而id又可以很方便地绑定具体用户,所以我们可以把不同请求归类到同一用户。基于这个方案,为了让用户每个请求都携带同一个id,在不妨碍体验的情况下,cookie是很好的载体。当用户首次访问系统时,系统会为该用户生成一个sessionId,并添加到cookie中。在该用户的会话期内,每个请求都自动携带该cookie,因此系统可以很轻易地识别出这是来自哪个用户的请求。
尽管cookie非常有用,但有时用户会在浏览器中禁用它,可能是出于安全考虑,也可能是为了保护个人隐私。在这种情况下,基于cookie实现的sessionId自然就无法正常使用了。因此,有些服务还支持用URL重写的方式来实现类似的体验,例如:
http://localhost:8080;jsessionid=xxxx
URL重写原本是为了兼容禁用cookie的浏览器而设计的,但也容易被黑客利用。黑客只需访问一次系统,将系统生成的sessionId提取并拼凑在URL上,然后将该URL发给一些取得信任的用户。只要用户在session有效期内通过此URL进行登录,该sessionId就会绑定到用户的身份,黑客便可以轻松享有同样的会话状态,完全不需要用户名和密码,这就是典型的会话固定攻击。
会话过期
除防御会话固定攻击外,还可以通过Spring Security配置一些会话过期策略。例如,会话过期时跳转到某个URL。
.sessionManagement().sessionFixation().none().invalidSessionUrl("/session/invalidUrl")
或者完全自定义过期策略。
.and().sessionManagement().invalidSessionStrategy(new InvalidSessionStrategy() { @Override public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { response.setContentType("text/html;charset=utf-8"); response.getWriter().write("session无效"); } })
默认情况下,只要该会话在30分钟内没有活动便会失效,失效后再尝试发起访问,将会得到如图6-2所示的应答。
当然,我们可以手动修改会话的过期时间。
server: servlet: session: timeout: 30
会话的过期时间最少为1分钟,所以即便设置小于60秒也会被修正为1分钟,这属于Spring Boot的配置策略。
会话并发控制
固定会话攻击和会话过期策略都很简单,在SpringSecurity中,会话管理最完善的是会话并发控制,但会话并发控制存在一些用法陷阱,应当多加注意,下面来看看详细用法。
一个最简单的控制会话并发数的配置如下(为了避开陷阱,先启用基于内存的用户配置)。