本文记录的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