1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > Spring Security中的会话【Session】管理与防御以及会话的并发控制

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

时间:2019-06-27 02:49:52

相关推荐

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

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

一、会话固定攻击

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

/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()));// 判断用户是否存在pute(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);puteIfPresent(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(paring(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。因为在会话并发控制,三个状态都是通过三个模块进行控制。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。