首页 > 其他分享 >spring security中对并发登录的处理

spring security中对并发登录的处理

时间:2024-06-08 22:12:07浏览次数:26  
标签:登录 spring request 并发 sessionId session security public

本文记录的spring security中对并发登录的处理,是基于使用session进行登录的场景,并且只适用于单体部署的场景

一、session管理策略接口 SessionAuthenticationStrategy

针对同一个账号多次登录的问题,spring security抽象出了一个接口来处理同一个用户的多个session

public interface SessionAuthenticationStrategy {

	void onAuthentication(Authentication authentication, HttpServletRequest request,
			HttpServletResponse response) throws SessionAuthenticationException;

}

spring security管理session的原理是在内存中维护了一个map,存储当前账号的已登录session对象,当一个账号的已登录session个数到达指定的个数时触发指定的策略。

其中有如下几个比较重要的实现:

1.1 RegisterSessionAuthenticationStrategy

这个实现向SessionRegistry 中注册当前登录账号的一个新session。

public class RegisterSessionAuthenticationStrategy implements
		SessionAuthenticationStrategy {
    //内部维护了一个map存储某个账号已经登录的session
	private final SessionRegistry sessionRegistry;

	public RegisterSessionAuthenticationStrategy(SessionRegistry sessionRegistry) {
		Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
		this.sessionRegistry = sessionRegistry;
	}

	// 此方法向sessionRegistry中注册了某账号的一个session
	public void onAuthentication(Authentication authentication,
			HttpServletRequest request, HttpServletResponse response) {
		sessionRegistry.registerNewSession(request.getSession().getId(),
				authentication.getPrincipal());
	}
}

所以session控制的关键在SessionRegistry ,它内部维护了一个账号的已登录session,它是一个接口,spring security只提供了一个实现 SessionRegistryImpl

1.2 SessionRegistryImpl

部分源码分析

public class SessionRegistryImpl implements SessionRegistry,
ApplicationListener<SessionDestroyedEvent> {
    //这个map中key是用户标识,value是当前用户已登录的sessionId的集合
    private final ConcurrentMap<Object, Set<String>> principals;
    
    // 存储sessionId 和session信息的对应关系
	private final Map<String, SessionInformation> sessionIds;
    
    //这个方法注册principal的一个新session,principal表示用户信息
	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 (logger.isDebugEnabled()) {
			logger.debug("Registering session " + sessionId + ", for principal "
					+ principal);
		}
		//如果sessionId在当前类中已注册过session信息就清除它
		if (getSessionInformation(sessionId) != null) {
			removeSessionInformation(sessionId);
		}
		//保存session信息
		sessionIds.put(sessionId,
				new SessionInformation(principal, sessionId, new Date()));
		//从principals中获取用户principal的已登录sessionid的集合,如果获取不到会返回一个新集合
		Set<String> sessionsUsedByPrincipal = principals.computeIfAbsent(principal, key -> new CopyOnWriteArraySet<>());
        //往集合中添加一个新的已登录sessionId
		sessionsUsedByPrincipal.add(sessionId);

		if (logger.isTraceEnabled()) {
			logger.trace("Sessions used by '" + principal + "' : "
					+ sessionsUsedByPrincipal);
		}
	}
    //获取给定sessionId在当前类中对应的session信息
    public SessionInformation getSessionInformation(String sessionId) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");

		return sessionIds.get(sessionId);
	}
    // 删除某个session的注册信息
	public void removeSessionInformation(String sessionId) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");
		//获取sessionId对应的session信息,信息中会有用户的账号等信息
		SessionInformation info = getSessionInformation(sessionId);

		if (info == null) {
			return;
		}

		if (logger.isTraceEnabled()) {
			logger.debug("Removing session " + sessionId
					+ " from set of registered sessions");
		}
		//在存储session信息的集合中删除
		sessionIds.remove(sessionId);
		// 根据info中的用户信息,获取此用户已登录的sessionId集合,从其中删除要删除的这个sessionId
		Set<String> sessionsUsedByPrincipal = principals.get(info.getPrincipal());

		if (sessionsUsedByPrincipal == null) {
			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Removing session " + sessionId
					+ " from principal's set of registered sessions");
		}
		//删除要删除的sessionId
		sessionsUsedByPrincipal.remove(sessionId);

		if (sessionsUsedByPrincipal.isEmpty()) {
			// No need to keep object in principals Map anymore
			if (logger.isDebugEnabled()) {
				logger.debug("Removing principal " + info.getPrincipal()
						+ " from registry");
			}
            //如果删除后此用户已经没有已登录的session了就把他从
			principals.remove(info.getPrincipal());
		}

		if (logger.isTraceEnabled()) {
			logger.trace("Sessions used by '" + info.getPrincipal() + "' : "
					+ sessionsUsedByPrincipal);
		}
	}
}

1.3 ConcurrentSessionControlAuthenticationStrategy

这个策略处理一个账户进行多次登录的情况,它内部持有SessionRegistry,可以获取到某个账号已经登录过的session信息,并根据已登录的session个数做出处理。

ConcurrentSessionControlAuthenticationStrategy源码分析

public class ConcurrentSessionControlAuthenticationStrategy implements
		MessageSourceAware, SessionAuthenticationStrategy {
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    //使用这个对象在内存中维护当前账号已登录的session
	private final SessionRegistry sessionRegistry;
    // 已登录session已到达上限时是否抛出异常
	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) {
		//获取当前账号已登录的session信息
		final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
				authentication.getPrincipal(), false);

		int sessionCount = sessions.size();
        //获取允许的session个数
		int allowedSessions = getMaximumSessionsForThisUser(authentication);

		if (sessionCount < allowedSessions) {
			// They haven't got too many login sessions running at present
			return;
		}
		//-1表示没有限制登录个数
		if (allowedSessions == -1) {
			// We permit unlimited logins
			return;
		}
		// 这个if成立表示session个数已经到达限制
		if (sessionCount == allowedSessions) {
			HttpSession session = request.getSession(false);
			//当前请求存在session时在判断下已经登录的session里是不是有当前session,如果有就返回
             //这个方法里返回就表示session策略校验通过了
			if (session != null) {
				// Only permit it though if this request is associated with one of the
				// already registered sessions
				for (SessionInformation si : sessions) {
					if (si.getSessionId().equals(session.getId())) {
						return;
					}
				}
			}
		}
		//能走到这里表示session个数已经超了,在这个方法里去判断要不要抛异常,后续如何处理
		allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
	}
    
	protected void allowableSessionsExceeded(List<SessionInformation> sessions,
			int allowableSessions, SessionRegistry registry)
			throws SessionAuthenticationException {
        //如果允许抛出异常这里就会抛出异常,登录就会失败
		if (exceptionIfMaximumExceeded || (sessions == null)) {
			throw new SessionAuthenticationException(messages.getMessage(
					"ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
					new Object[] { Integer.valueOf(allowableSessions) },
					"Maximum sessions of {0} for this principal exceeded"));
		}

		// Determine least recently used session, and mark it for invalidation
        //走到这里表示配置的不允许抛出异常,会找出上次没有使用的时间最早的一个session让
        //它过期
		SessionInformation leastRecentlyUsed = null;

		for (SessionInformation session : sessions) {
			if ((leastRecentlyUsed == null)
					|| session.getLastRequest()
							.before(leastRecentlyUsed.getLastRequest())) {
				leastRecentlyUsed = session;
			}
		}
		//让这个session过期
		leastRecentlyUsed.expireNow();
	}
    
}

二、session管理策略的应用时机

以spring security自带的UsernamePasswordAuthenticationFilter 为例,session管理策略在其doFilter方法中,认证成功后就会被应用,这种设计也是合理的,账户密码认证成功后根据session管理策略来决定要不要让当前这次登录成功。

UsernamePasswordAuthenticationFilter的doFilter方法在其父类AbstractAuthenticationProcessingFilter中

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
             //调用子类方法对本次登录请求进行认证
			authResult = attemptAuthentication(request, response);
             // 返回的不是空说明认证成功
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
             // 认证成功后开始应用session管理策略,看session是否超限
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// 登录成功后是否继续执行后续过滤器,默认情况下是false,跳过后续过滤器
        // 后续过滤器中有一个SessionManagementFilter也会应用上边的session策略,但因为
        // 跳过后续过滤器了,就不会被执行到。而这个值在大部分应用中也不会改,所以上边对session策略
        // 的使用就变成了唯一一次。
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		// 登录成功的后续处理,包括更新securityContext,按成功的处理策略做出响应
		successfulAuthentication(request, response, chain, authResult);
	}

三、如何配置

当使用spring security提供的UsernamePasswordAuthenticationFilter 进行登录处理时,默认情况下并不会对

session的个数做限制,如果需要调整允许的session个数需要在WebSecurityConfigurerAdapter 的实现类中进行配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //安全配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //匹配路径时越具体的路径要先匹配
        //放行登录页面,和处理登录请求的url
        http.authorizeRequests().antMatchers("/login", "/login.html").permitAll()
                        .antMatchers("/hello/test1").hasRole("P1").and()
                        .formLogin().loginPage("/login.html").loginProcessingUrl("/login")
                .and().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
    }
}

上面这个配置中使用sessionManagement进行配置,允许的session个数是1,已登录session到达限制时会抛出异常。

四、自定义登录过滤器时如何配置session个数控制

注意上边的配置只有在使用spring security的UsernamePasswordAuthenticationFilter 进行登录控制时才生效。

为什么呢?

考虑上边提到的这个策略的应用时机是在登录过滤器完成登录验证后在父类方法中应用,如果你自定义的过滤器没有继承自AbstractAuthenticationProcessingFilter,那自然就不会有这么一个步骤。一般在自定义登录过滤器中完成登录后就会给前端做出响应。

所以这样你写的session配置其实就是不生效的。

解决办法:在自定义过滤器中登录校验完成后使用下session策略。

MyLoginFilter继承了AbstractAuthenticationProcessingFilter,所以就会有校验session策略的逻辑。

public class MyLoginFilter extends AbstractAuthenticationProcessingFilter {
    protected MyLoginFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        //校验逻辑
        return null;
    }
}

但又会有另一个问题,父类的sessionStrategy如何赋值,自定义过滤器一般都是我们自己创建对象配置到过滤器链中,

父类提供了一个set方法可以赋值,那赋值的策略从哪里获取呢?

public void setSessionAuthenticationStrategy(
			SessionAuthenticationStrategy sessionStrategy) {
		this.sessionStrategy = sessionStrategy;
	}

如果你的项目需要自己实现session的并发控制策略,你可以自定义一个策略接口的实现类,创建对象赋值给你的登录过滤器然后配置到过滤器链中。(如何配置过滤器链请参考我的其他博客)

如果还想使用用sessionManagement方法配置的那个策略,需要自定义spring security的configure来配置我们自定义的过滤器

import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

public class MyLoginConfigure extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Override
    public void configure(HttpSecurity builder) throws Exception {
        //在这里创建过滤器并设置session策略
        MyLoginFilter filter = new MyLoginFilter("/");
        // spring security启动过程中会设置这么一个共享对象,在这里可以拿到,具体原理可以关注我关于 security的其他文章
        SessionAuthenticationStrategy sharedObject = builder.getSharedObject(SessionAuthenticationStrategy.class);
        //设置策略
        filter.setSessionAuthenticationStrategy(sharedObject);
        //添加过滤器
        builder.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

那这个configure在哪里用呢,

在WebSecurityConfigurerAdapter的实现类中configure方法中使用,如下

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //安全配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //匹配路径时越具体的路径要先匹配
        //放行登录页面,和处理登录请求的url
        http.authorizeRequests().antMatchers("/login", "/login.html").permitAll()
                        .antMatchers("/hello/test1").hasRole("P1").and()
                        .formLogin().loginPage("/login.html").loginProcessingUrl("/login")
                .and().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
        //应用自定义的configurer
        http.apply(new MyLoginConfigure());
    }
}

标签:登录,spring,request,并发,sessionId,session,security,public
From: https://www.cnblogs.com/chengxuxiaoyuan/p/18239017

相关文章

  • Spring Boot、MongoDB、Vue 2和Nginx实现一个小说网站
    在本篇文章中,我们将带你逐步实现一个完备的小说网站项目,技术栈包括SpringBoot、MongoDB、Vue2和Nginx。1.项目概述我们将实现一个基本的小说网站,包含以下主要部分:后端API:使用SpringBoot实现,负责处理数据和业务逻辑。数据库:使用MongoDB存储小说数据。前端页面:使用Vue2实......
  • 【建站教程】Ubuntu结合宝塔面板本地部署Inis博客并发布公网
    ......
  • 【计算机论文指导】基于Spring boot食品安全信息管理系统
    摘要食品安全信息管理系统设计的目的是为用户提供食品信息、科普专栏、食品检测、检测结果、交流论坛等方面的平台。与PC端应用程序相比,食品安全信息管理系统的设计主要面向于用户,旨在为管理员和用户提供一个食品安全信息管理系统。用户可以通过APP及时查看食品信息、新......
  • Springboot 开发 -- Redis实现分布式Session
    一、引言在微服务架构和分布式系统中,会话管理(SessionManagement)成为了一个挑战。传统的基于Servlet容器的会话管理方式在分布式环境下无法有效工作,因为用户请求可能会被分发到不同的服务器上,导致会话数据无法共享。为了解决这个问题,SpringSession提供了一种基于外部存储(......
  • 【计算机毕业设计】springboot027网上点餐系统
    随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势;对于网上点餐系统当然也不能排除在外,随着网络技术的不断成熟,带动了网上点餐系统,它彻底改变了过去传统的管理方式,不仅使服务管理难度变低了,还提升了管理的灵活性。这种个性化的平台特别......
  • 【计算机毕业设计】springboot030甘肃非物质文化网站的设计与开发
    现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本甘肃非物质文化网站就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息,使用这种软件工具可以帮助管理人员提高事务处理效率,......
  • 【计算机毕业设计】springboot031教师工作量管理系统
    随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了教师工作量管理系统的开发全过程。通过分析教师工作量管理系统管理的不足,创建了一个计算机管理教师工作量管理系统的方案。文章介绍了教师工作量管理系统的系统分析部分,包括可......
  • 【计算机毕业设计】springboot032阿博图书馆管理系统
    随着社会的发展,计算机的优势和普及使得阿博图书馆管理系统的开发成为必需。阿博图书馆管理系统主要是借助计算机,通过对图书借阅等信息进行管理。减少管理员的工作,同时也方便广大用户对所需图书借阅信息的及时查询以及管理。阿博图书馆管理系统的开发过程中,采用B/S架构,主......
  • 小白之路之SpringBoot框架
    在最近学习闲暇时刻突然爆发灵感想要自己写一个通用的SpringBoot项目出来,目前使用了到jwt+redis+mybatisPlus嗯差不多这些吧。整个项目的目录框架这个结构差不多也是在网上到处缝缝补补(以致于可能是SpringBoot还是SpringCloud都有一点分不清除了),暂时分为了两个模块1、com......
  • 一文了解 - -> SpringMVC
    一、SpringMVC概述SpringMVC是由Spring官方提供的基于MVC设计理念的web框架。SpringMVC是基于Servlet封装的用于实现MVC控制的框架,实现前端和服务端的交互。1.1SpringMVC优势严格遵守了MVC分层思想采用了松耦合、插件式结构;相比较于我们封装的BaseServlet以及其他的......