Spring Security中的会话【Session】管理与防御以及会话的并发控制

article/2025/9/18 3:40:41

       众所周知,HTTP本身是没有任何关于当前用户状态的内容,也就是两个HTTP请求之间是没有任何的关联可言,用户在和服务器交互的过程中,站点无法确定是哪个用户访问,也因此不能对其提供相应的个性化服务。Session的诞生就是为了解决这一个难题,提供了无状态的HTTP请求实现用户状态维持的方案——服务器和用户之间约定每个HTTP请求携带一个ID信息【代表当前用户信息】,从而实现不同的请求之间就存在着关联。当用户首次访问时,会自动为该用户生成一个sessionId,然后用Cookie作为载体进行记录,用户在会话周期中间访问都会带上Cookie中的内容,因此系统就可以识别出是哪一个用户。

一、会话固定攻击

        尽管Cookie是非常有用的,但是有些用户出于安全或者保护个人隐私的目的,会关闭Cookie,如上图Google Chrome中的设置。在这种情况下,就不能使用基于Cookie实现Session,这样就使得用户体验不太好。所以针对于这一点,有些网站提供URL重写来实现类似的体验。但是这种情况下会存在问题:URL重写会直接将SessionId拼接在URL上,即例如下面所示。

http://www.test.com/test;jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764

        这种方式很容易被黑客进行利用,黑客只需要访问一次站点,将系统生成的SessionId粘贴到这个URL上面,然后将该URL放给用户,只要用户在session有效期内通过此URL进行登录,该sessionId就会绑定到用户的身份,黑客便可以轻松享有同样的会话状态,完全不需要用户名和密码,这就是典型的会话固定攻击。

        会话固定攻击的防御方式其实是很简单,即用户登录后就刷新SessionId,这样原先的SessionId就失去作用。在SpringSecurity中,默认帮我们开启了这种方式,因此并不需要我们特别配置。但是我们也可以手动配置,在Spring Security中的防御会话固定攻击的策略有四种:

newSession: 登录之后创建一个新的session

migrateSession: 登录之后创建一个新的session,并将旧的session中的数据复制过来。

changeSessionId:不创建新的会话,而是使用由Servlet容器提供的会话固定保护。

none: 不做任何变动,登录之后沿用旧的session

        其实这四种策略对应以下四个方法的的三个对象:SessionFixationProtectionStrategy、ChangeSessionIdAuthenticationStrategy、NullAuthenticatedSessionStrategy。

         配置的方式也是很简单,在我下载的Spring Security 5.2.8版本中默认指定的是changeSessionId。

         除了这种改变SessionId的值,也可以通过设置会话过期策略的方式来防御。默认的情况下,会话过期时间是30分钟。也可以指定失效的策略。

二、会话的并发控制

        对于会话并发控制这一概念,在我们经常使用的视频软件中有所体现,例如腾讯视频和爱奇艺,若我的账号购买了会员,那我可以将这个账号分享给我的朋友、家人。当然这种肯定不能无限制的登录,就像对于腾讯视频和爱奇艺,如果不限制登录账号的设备数量,那肯定就亏惨了,而且也不利于自身信息的安全。所以会限制同时登录的设备数量,一旦超过这个限制,前面的账户就会被踢下来,这就是所谓的并发控制。

//session相关的控制
.sessionManagement()//指定最大的session并发数量 .maximumSessions(1) 

        会话的并发数量设置很简单,使用maximumSessions即可。如果没有额外的配置,重新登录的会话会踢掉旧的会话。在介绍会话并发之前需要理解一个叫做SessionRegistry的对象——管理用户的会话状态,也可以称作为用户会话信息表。之所以可以称作为用户会话信息表,是因为其中维护着两个ConcurrentHashMap对象principals和sessionIds,分别存储着主体Principal和会话信息SessionInformation。Principal在之前的文章中说过是包含主体的信息,SessionInformation其实就是包括了主体信息、sessionId、是否过期以及上次请求时间。

       SessionInformation的主要作用是在Spring Security中并发控制中记录Session的信息。Session的在Security中有三个状态:Active(活跃)、Expired(过期)以及Destroyed(无效)。让一个Session无效可以通过Session本身的invalidate方法使其失效,也可以通过Servlet容器管理进行销毁。Session过期很大程度上是由于用户的最大会话数已经达到限制,此时就必须使会话过期,过期的会话会通过过滤器很快的就被删除。

        有了SessionInformation的理解后,我们再来看SessionRegistryImpl这个实现SessionRegistry的类,对会话信息表的具体操作也是在这个实现类中。

public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> {protected final Log logger = LogFactory.getLog(SessionRegistryImpl.class);// 用户及其对应Session,一个用户可能有多个sessionprivate final ConcurrentMap<Object, Set<String>> principals;// SessionId及其对应的SessionInformationprivate final Map<String, SessionInformation> sessionIds;public SessionRegistryImpl() {this.principals = new ConcurrentHashMap();this.sessionIds = new ConcurrentHashMap();}public SessionRegistryImpl(ConcurrentMap<Object, Set<String>> principals, Map<String, SessionInformation> sessionIds) {this.principals = principals;this.sessionIds = sessionIds;}// 获取当前的所有主体信息public List<Object> getAllPrincipals() {return new ArrayList(this.principals.keySet());}// 获取主体对应的会话信息,可包含过期或者不过期的SessionInformationpublic List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {// 获取当前主体的所有会话IDSet<String> sessionsUsedByPrincipal = (Set)this.principals.get(principal);if (sessionsUsedByPrincipal == null) {return Collections.emptyList();} else {List<SessionInformation> list = new ArrayList(sessionsUsedByPrincipal.size());Iterator var5 = sessionsUsedByPrincipal.iterator();while(true) {SessionInformation sessionInformation;do {// 获取对应ID的SessionInformation,若没有则继续获取,循环结束则直接返回Listdo {if (!var5.hasNext()) {return list;}String sessionId = (String)var5.next();sessionInformation = this.getSessionInformation(sessionId);} while(sessionInformation == null);// 查询是否过期的且当前SessionInformation是否过期} while(!includeExpiredSessions && sessionInformation.isExpired());// 满足条件则添加至Listlist.add(sessionInformation);}}}// 根据ID获取对应的SessionInformationpublic SessionInformation getSessionInformation(String sessionId) {Assert.hasText(sessionId, "SessionId required as per interface contract");return (SessionInformation)this.sessionIds.get(sessionId);}// 实现onApplicationEvent接口,表明处理SessionDestoryedEvent事件public void onApplicationEvent(SessionDestroyedEvent event) {String sessionId = event.getId();// 移除对应sessionId的相关数据this.removeSessionInformation(sessionId);}// 刷新最近操作日期public void refreshLastRequest(String sessionId) {Assert.hasText(sessionId, "SessionId required as per interface contract");SessionInformation info = this.getSessionInformation(sessionId);if (info != null) {info.refreshLastRequest();}}// 新增会话信息// SessionManagementConfigure默认会将RegisterSessionAuthenticationStrategy添加// 到一个组合式的SessionAuthenticationStrategy中,// 并由AbstractAuthenticationProcessingFilter在成功调用时,触发该动作。public void registerNewSession(String sessionId, Object principal) {Assert.hasText(sessionId, "SessionId required as per interface contract");Assert.notNull(principal, "Principal required as per interface contract");// 若存在,则先删除会话信息if (this.getSessionInformation(sessionId) != null) {this.removeSessionInformation(sessionId);}if (this.logger.isDebugEnabled()) {this.logger.debug("Registering session " + sessionId + ", for principal " + principal);}// 会话信息this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));// 判断用户是否存在this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {if (sessionsUsedByPrincipal == null) {sessionsUsedByPrincipal = new CopyOnWriteArraySet();}// 若用户存在,将当前sessionId添加到对应的集合中。// 用Set即可实现去重((Set)sessionsUsedByPrincipal).add(sessionId);if (this.logger.isTraceEnabled()) {this.logger.trace("Sessions used by '" + principal + "' : " + sessionsUsedByPrincipal);}return (Set)sessionsUsedByPrincipal;});}// 删除会话信息public void removeSessionInformation(String sessionId) {Assert.hasText(sessionId, "SessionId required as per interface contract");SessionInformation info = this.getSessionInformation(sessionId);if (info != null) {if (this.logger.isTraceEnabled()) {this.logger.debug("Removing session " + sessionId + " from set of registered sessions");}// 以String类型的Key删除对应的sessionId及其Informationthis.sessionIds.remove(sessionId);this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {if (this.logger.isDebugEnabled()) {this.logger.debug("Removing session " + sessionId + " from principal's set of registered sessions");}// 将用户会话记录中的,对应用户的对应session删除sessionsUsedByPrincipal.remove(sessionId);// 如果获取成功,则清理对应的内容if (sessionsUsedByPrincipal.isEmpty()) {if (this.logger.isDebugEnabled()) {this.logger.debug("Removing principal " + info.getPrincipal() + " from registry");}sessionsUsedByPrincipal = null;}if (this.logger.isTraceEnabled()) {this.logger.trace("Sessions used by '" + info.getPrincipal() + "' : " + sessionsUsedByPrincipal);}return sessionsUsedByPrincipal;});}}
}

        对于SessionRegistryImpl我们还有额外注意的几点,若是稍不注意很容易在后续的使用过程中碰壁。

        第一点,对象中的Principals采用以用户信息为Key。而在HashMap中,以对象为Key必须覆写hashCode和equals两个方法,因此在自己实现UserDetails时必须重写这两个方法,若没有重写会导致同一个用户每次登录注销时计算得到的Key都不相同,所以每次登录都会向Principals中添加一个用户,而注销时却从来不能有效移除。这种情况下,不仅达不到会话并发控制的效果,还会引起内存泄露。

        第二点,我们注意到SessionRegistryImpl其实是实现了接口ApplicationListener的。在Servlet中监听Session相关事件的方法是实现HttpSessionListener接口,并在系统中注册该监听器。而SpringSecurity中在HttpSessionEventPublisher类中实现HttpSessionEventPublisher接口,并转换成Spring的事件机制,从而也就有了SessionDestroyedEvent这个事件,即会话的销毁事件。所以为了要实现事件的监听,就必须将HttpSessionEventPublisher注册到IOC容器中,这样才能将Java时间转换为Spring事件【只要使用会话管理功能,就应该配置HttpSessionEventPublisher】。

 

        有了上面的理解,此时我们再来分析并发控制的策略。看完下面的并发控制策略后,其实会发现这里面只有控制当超过会话数量时,使会话的状态过期的操作。当时并没有进行会话的注册和删除。这也就是上面第二点所说,Security中会话创建和删除事件都是通过Spring的事件机制实现的,我们在SessionRegistryImpl同一个包中也可以看到Creation和Destoryed分别都有对应的事件,通过这两个事件才实现注册、销毁。

public class ConcurrentSessionControlAuthenticationStrategy implements MessageSourceAware, SessionAuthenticationStrategy {protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();private final SessionRegistry sessionRegistry;// 如果超出最大会话数是否阻止新会话的建立private boolean exceptionIfMaximumExceeded = false;// 最大的会话数private int maximumSessions = 1;public ConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) {Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");this.sessionRegistry = sessionRegistry;}public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {// 获取当前用户的所有有效的会话信息List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);int sessionCount = sessions.size();int allowedSessions = this.getMaximumSessionsForThisUser(authentication);// 判断当前用户的会话数量是否超过最大值if (sessionCount >= allowedSessions) {// 如果最大会话数量为-1,则默认不限制会话数量if (allowedSessions != -1) {// 当已存在的会话数量等于最大会话数时if (sessionCount == allowedSessions) {// 判断当前会话是否已经在用户对应的会话列表中HttpSession session = request.getSession(false);if (session != null) {Iterator var8 = sessions.iterator();while(var8.hasNext()) {SessionInformation si = (SessionInformation)var8.next();// 当前验证的会话并不是新的会话,则不做任何的处理if (si.getSessionId().equals(session.getId())) {return;}}}}// 进行策略判断this.allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);}}}......protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException {// 当用户达到最大会话数时,是否阻止新会话的建立if (!this.exceptionIfMaximumExceeded && sessions != null) {// 按照建立会话时间先后升序排序,sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));// 取待过期的会话 int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);Iterator var6 = sessionsToBeExpired.iterator();while(var6.hasNext()) {// 新会话建立,使最早的会话过期SessionInformation session = (SessionInformation)var6.next();session.expireNow();}} else {// 提示会话已超过数量throw new SessionAuthenticationException(this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded"));}}......
}

        通过上面的分析,也可以让我们理解为什么官网对于SessionInformation的解释中一开始就提出了Session的三个状态:Active、Expired、Destoryed。因为在会话并发控制,三个状态都是通过三个模块进行控制。


http://chatgpt.dhexx.cn/article/5arpWN5t.shtml

相关文章

web基础:会话

一、会话概述 HttpSession对象可以保存跨同一个客户多个请求的会话状态。即与一个特定客户的整个会话期间&#xff0c;HttpSession会持久存储。对于会话期间客户做的所有请求&#xff0c;从中得到的所有信息&#xff0c;都可以使用HttpSession对象保存。 会话的工作方式&#…

会话固定攻击(session fixation attack)及解决办法

1 Cookie 的工作过程 Cookie的传递用到了两个字段: 请求头字段Cookie和响应头字段Set-Cookie。 当用户浏览器第一次访问服务器的时候&#xff0c;服务器肯定是不知道他的身份的。所以&#xff0c;就要创建一个独特的身份识别数据&#xff0c;格式是"keyvalue"&…

2022年会话推荐综述

title: 2022年会话推荐综述 最近对于会话推荐有了新的兴趣 文章题目&#xff1a; A Survey on Session-based Recommender Systems 0. 前言 提供了一个统一的框架来对SBRSs研究进行分类 SBRS的统一问题陈述&#xff0c;其中SBRS建立在正式概念之上&#xff1a;用户、项目、…

会话保持原理

1. 什么是会话保持&#xff1f; 会话保持是负载均衡最常见的问题之一&#xff0c;也是一个相对比较复杂的问题。会话保持有时候又叫做粘滞会话(Sticky Sessions)。会话保持是指在负载均衡器上的一种机制&#xff0c;可以识别客户端与服务器之间交互过程的关连性&#xff0c;在…

linux会话session

linux会话 什么是linux session 我们常见的 Linux session 一般是指 shell session。Shell session 是终端中当前的状态&#xff0c;在终端中只能有一个 session。 当我们打开一个新的终端时&#xff0c;总会创建一个新的 shell session。这表明会话是我们和shell交互的一个过…

会话令牌写入URL

目录 一. 漏洞描述 二. 漏洞修复 一. 漏洞描述 会话令牌即Token&#xff0c;关于Token&#xff0c;传送门-》如何理解Session、Cookie与Token 。 用户在进行get请求将用户的令牌写入url&#xff0c;导致中间人攻击获取令牌进行登陆。如下 二. 漏洞修复 禁止将令牌写入url

PDU会话流程

1.PDU会话的概念 1.1概述 5G系统的一个关键功能&#xff1a;为UE提供一个到达DN的连接会话管理功能&#xff08;SMF&#xff09;职责&#xff1a; 设置UE到DN的连接对该连接的用户面进行管理 5G系统的设计目标是支持大量的5G案例&#xff1a; 支持不同的PDU类型多个可选的…

会话固定漏洞

目录 漏洞原理 漏洞检测 漏洞利用 漏洞修复 漏洞原理 Session 是应用系统对浏览器客户端身份认证的属性标识&#xff0c;在用户退出应用系统时&#xff0c;应将客户端 Session 认证属性标识清空。如果未能清空客户端 Session 标识&#xff0c;在下次登录系统时&#xff0c…

《Oracle Java EE编程自学和面试指南》09-02:HttpSession接口

深入了解IT/互联网行业及岗位&#xff0c;请参阅通用IT/互联网岗位招聘计划&#xff08;最新全岗版&#xff09;。 深入了解职业晋升及学习路线&#xff0c;请参阅最优职业晋升路线和课程学习指南&#xff08;最新全栈版&#xff09;。 内容导航&#xff1a; 前言1、HttpSessi…

【LWIP的mDNS】

一&#xff0e;mdns 1.什么是mdns&#xff1f; mDNS协议适用于局域网内没有DNS服务器时的域名解析&#xff0c;设备通过组播的方式交互DNS记录来完成域名解析&#xff0c;约定的组播地址是&#xff1a;224.0.0.251&#xff0c;端口号是5353 主要用于在同一局域网内&#xff0c;…

DNS DDNS NBNS mDNS LLMNR LLDPDU SSDP协议

DNS DNS只是提供了域名和IP地址之间的静态对应关系&#xff0c;当IP地址发生变化时&#xff0c;DNS无法动态的更新域名和IP地址之间的对应关系&#xff0c;从而导致访问失败。但是DDNS系统是将用户的动态IP地址映射到一个固定的域名解析服务上 DDNS DDNS用来动态更新DNS服务…

mDNSResponder介绍与移植

mDNSResponder是苹果的Bonjour项目的一部分。 Bonjour是法语“你好”的意思。 Bonjour软件源自正IETF零配置网络工作。零配置工作有三个要求&#xff1a; 1.分配IP地址&#xff08;即使没有分配DHCP服务器的IP地址&#xff09; 2.提供名称到地址的转换&#xff08;即使没有DNS服…

启用Ubuntu 服务器上的 mDNS

陈拓 2022/03/18-2022/03/18 在动态分配IP地址的情况下&#xff0c;在局域网中使用mDNS访问Ubuntu服务器就不需要知道IP地址了。 我的Ubuntu系统版本 先用IP地址登录Ubuntu服务器。 具体操作见《Win10命令窗口的SSH和SFTP操作》 Win10命令窗口的SSH和SFTP操作_晨之清风的博…

Bonjour手把手搭建一:mDNS(apple multicastdns.org)

mDNS(Multicast DNS)——From Apple https://support.apple.com/kb/TA20999?localezh_CN&viewlocaleen_US Multicast DNS, one of the features incorporated in Bonjour, which was introduced in Mac OS X 10.2. Bonjour的一个新特性&#xff0c;在Mac OS X10.2后引入…

SpyCast:一款功能强大的跨平台mDNS枚举工具

关于SpyCast SpyCast是一款功能强大的跨平台mDNS枚举工具&#xff0c;该工具支持在主动模式下下递归查询服务&#xff0c;也可以在被动模式下仅侦听多播数据包。因此&#xff0c;广大研究人员可以使用该工具测试mDNS协议和本地网络的安全性。 mDNS介绍 mDNS&#xff0c;即多…

esp-idf编译报错Failed to resolve component ‘mdns‘

Failed to resolve component mdns 根据提示 打开https://components.espressif.com/搜索mdns 选择复制 执行后再次进行编译

OpenWrt 安装 mDNS,并设置 mDNS 映射

OpenWrt 安装 mDNS&#xff0c;并设置 mDNS 映射 路由器&#xff1a;OpenWrt 21.02.1电脑&#xff1a;Windows 10 21H1 Windows 10 现在已经默认支持了 mdns&#xff0c;可以直接 ping 设备名称.local 查看电脑的ip。 1.下载 mDNS 安装包 opkg update opkg install umdns…

ESP mDNS 学习

文章目录 1 概述2 linux avahi3 ESP32 mDNSmDNS 数据包4 参考资料 1 概述 mDNS 是一种多播 UDP 服务&#xff0c;用来提供本地网络服务和主机发现&#xff0c;mDNS 协议发布为 rfc6762。 在没有任何传统单播 DNS 服务器的情况下&#xff0c;多播 DNS (mDNS) 提供了在本地连接…

mDNS/DNS-SD java及Avahi 实现服务发布和服务发现

一、jmDNS&#xff1a;JAVA实现&#xff08;JmDNS&#xff09; 引入pom <!-- https://mvnrepository.com/artifact/javax.jmdns/jmdns --> <dependency><groupId>javax.jmdns</groupId><artifactId>jmdns</artifactId><version>3.4.…

mDNS安装与开启

在局域网建立.local域名 参考来源 1 苹果设备自带 Bonjour无需安装 2 windows需要安装 bonjour 装了也没用 安卓也没找到可用的 目前就苹果和乌班图可互通 3 linux设备 ubuntu 安装avahi sudo apt-get install avahi-daemon sudo apt-get install avahi-uti…