rememberme关闭浏览器时重新登录应用时无法登录

 

rememberme关闭浏览器时再重启应用时无法登录,提示达到最大登录数,需要等待session超时才能登录。

 

 

原因是SessionManagementFilter在调用ConcurrentSessionControlAuthenticationStrategy.onAuthentication()时,由于直接关闭浏览器,当时的session仍然保存在SessionRegistry中,在ConcurrentSessionControlAuthenticationStrategy进行验证时,误认为已经存在当前用户的活跃session,从而抛出最大登录数异常。

 

解决方法:

 

1. 设置<security:concurrency-control max-sessions="1" error-if-maximum-exceeded="false"/>,使用后登陆挤掉先登录用户的策略。

 

2. session靠Cookie来维持,每次给客户端一个cookie里面存放session id,然后请求的时候,服务器根据session id找到对应的session。这个cookie是在浏览器关闭的时候就失效了,自动登录的cookie需要设置成为关闭浏览器后cookie还有效。

 

3. 在当前窗口的关闭事件中发送请求主动使当前session失效,调用session.invalidate()。

 

4. 使用自定义的ConcurrentSessionControlAuthenticationStrategy、RegisterSessionAuthenticationStrategy和SessionRegistry的实现,保存用户的ip地址,在进行验证发现到达最大登录数时,在SessionRegistry中保存的当前用户的session id列表,判断是否存在session对应的ip与当前用户的ip相等,如果存在则使该session失效,并通过用户的验证。

package com.jaeson.springstudy.security;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.util.Assert;

public class MySessionRegistryImpl implements SessionRegistry,
		ApplicationListener<SessionDestroyedEvent> {

	private static final Logger logger = LoggerFactory.getLogger(MySessionRegistryImpl.class);
	
	/** <principal:Object,SessionIdSet> */
	private final ConcurrentMap<Object, Set<String>> principals = new ConcurrentHashMap<Object, Set<String>>();
	/** <sessionId:Object,SessionInformation> */
	private final Map<String, SessionInformation> sessionIds = new ConcurrentHashMap<String, SessionInformation>();
	//for ipAddress
	private final Map<String, String> ipAddr = new ConcurrentHashMap<String, String>();
	
	@Override
	public void onApplicationEvent(SessionDestroyedEvent event) {
		String sessionId = event.getId();
		removeSessionInformation(sessionId);
		logger.debug("onApplicationEvent fired, sessionId = {}", sessionId);
	}
	
	@Override
	public List<Object> getAllPrincipals() {
		return new ArrayList<Object>(principals.keySet());
	}

	@Override
	public List<SessionInformation> getAllSessions(Object principal,
			boolean includeExpiredSessions) {
		final Set<String> sessionsUsedByPrincipal = principals.get(principal);

		if (sessionsUsedByPrincipal == null) {
			return Collections.emptyList();
		}

		List<SessionInformation> list = new ArrayList<SessionInformation>(
				sessionsUsedByPrincipal.size());

		for (String sessionId : sessionsUsedByPrincipal) {
			SessionInformation sessionInformation = getSessionInformation(sessionId);

			if (sessionInformation == null) {
				continue;
			}

			if (includeExpiredSessions || !sessionInformation.isExpired()) {
				list.add(sessionInformation);
			}
		}

		return list;
	}
	
	@Override
	public SessionInformation getSessionInformation(String sessionId) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");

		return sessionIds.get(sessionId);
	}


	@Override
	public void refreshLastRequest(String sessionId) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");

		SessionInformation info = getSessionInformation(sessionId);

		if (info != null) {
			info.refreshLastRequest();
		}
	}

	@Override
	public void registerNewSession(String sessionId, Object authentication) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");
		Assert.notNull(authentication, "Authentication required as per interface contract");

		if (logger.isDebugEnabled()) {
			logger.debug("Registering session " + sessionId + ", for authentication "
					+ authentication);
		}
		
		Authentication auth = null;
		Object principal = authentication;
		if (authentication instanceof Authentication) {
			auth = (Authentication) authentication;
			principal = auth.getPrincipal();
		}
		
		if (getSessionInformation(sessionId) != null) {
			removeSessionInformation(sessionId);
			//for ip address
			removeIpAddress(sessionId);
		}
		
		sessionIds.put(sessionId,
				new SessionInformation(principal, sessionId, new Date()));
		
		if (auth != null) {
			Object details = auth.getDetails();
			if (details instanceof WebAuthenticationDetails)
				addIpAddress(sessionId, ((WebAuthenticationDetails) details).getRemoteAddress());
		}

		Set<String> sessionsUsedByPrincipal = principals.get(principal);

		if (sessionsUsedByPrincipal == null) {
			sessionsUsedByPrincipal = new CopyOnWriteArraySet<String>();
			Set<String> prevSessionsUsedByPrincipal = principals.putIfAbsent(principal,
					sessionsUsedByPrincipal);
			if (prevSessionsUsedByPrincipal != null) {
				sessionsUsedByPrincipal = prevSessionsUsedByPrincipal;
			}
		}

		sessionsUsedByPrincipal.add(sessionId);

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

	@Override
	public void removeSessionInformation(String sessionId) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");

		SessionInformation info = getSessionInformation(sessionId);

		if (info == null) {
			return;
		}
		
		logger.debug("begin remove sessionId: {}", info.getSessionId());
		
		logger.debug("before remove sessionIds sessionIds.size() = {}, principals.size() = {}, ipAddress.size() = {} ", 
				sessionIds.size(), principals.size(), ipAddr.size());

		sessionIds.remove(sessionId);
		
		removeIpAddress(sessionId);

		logger.debug("after remove sessionIds sessionIds.size() = {}, principals.size() = {}, ipAddress.size() = {} ", 
				sessionIds.size(), principals.size(), ipAddr.size());
		

		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");
		}

		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");
			}
			principals.remove(info.getPrincipal());
		}

		logger.debug("after remove principals  sessionIds.size() = {}, principals.size() = {}, ipAddress.size() = {} ", 
				sessionIds.size(), principals.size(), ipAddr.size());
	}

	public String getIpAddress(String sessionId){
		
		if (ipAddr.containsKey(sessionId))
			return ipAddr.get(sessionId);
		return null;
	}
	
	protected void removeIpAddress(String sessionId) {
		
		if (ipAddr.containsKey(sessionId))
			ipAddr.remove(sessionId);
	}
	protected void addIpAddress(String sessionId, String ipAddress) {
		ipAddr.put(sessionId, ipAddress);
	}
}

 

 

package com.jaeson.springstudy.security;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.util.Assert;

public class MyConcurrentSessionControlAuthenticationStrategy implements
	MessageSourceAware, SessionAuthenticationStrategy {
	
	private static final Logger logger = LoggerFactory.getLogger(MyConcurrentSessionControlAuthenticationStrategy.class);
	
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private final SessionRegistry sessionRegistry;
	private boolean exceptionIfMaximumExceeded = false;
	private int maximumSessions = 1;

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

	public void onAuthentication(Authentication authentication,
			HttpServletRequest request, HttpServletResponse response) {

		final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
				authentication.getPrincipal(), false);

		int sessionCount = sessions.size();
		int allowedSessions = getMaximumSessionsForThisUser(authentication);

		if (sessionCount < allowedSessions) {
			// They haven't got too many login sessions running at present
			return;
		}

		if (allowedSessions == -1) {
			// We permit unlimited logins
			return;
		}
		
		
		if (sessionCount == allowedSessions) {
			HttpSession session = request.getSession(false);
		
			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;
					}
				}
			}
			// If the session is null, a new one will be created by the parent class,
			// exceeding the allowed number
		}
		
		//判断是否来自同一个ip的request,如果是则使同一个ip的用户session失效
		Object details = authentication.getDetails();
		if (details instanceof WebAuthenticationDetails && sessionRegistry instanceof MySessionRegistryImpl) {
			
			logger.debug("using MySessionRegistryImpl to detect where request from same ip address...");
			
			MySessionRegistryImpl mySessionRegistry = (MySessionRegistryImpl) sessionRegistry;
			String ipAddress = ((WebAuthenticationDetails) details).getRemoteAddress();
			logger.debug("request ip address: {} ", ipAddress);
			for (SessionInformation session : sessions) {
				
				logger.debug("user({}) login in session info:{}", authentication.getName(), session);
				if (ipAddress.equals(mySessionRegistry.getIpAddress(session.getSessionId()))) {
					
					logger.debug("session: {} expired now", session.getSessionId());
					session.expireNow();
					return;
				}
			}
		}
		
		allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
	}


	protected int getMaximumSessionsForThisUser(Authentication authentication) {
		return maximumSessions;
	}

	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
		SessionInformation leastRecentlyUsed = null;
		
		for (SessionInformation session : sessions) {
			if ((leastRecentlyUsed == null)
					|| session.getLastRequest()
							.before(leastRecentlyUsed.getLastRequest())) {
				leastRecentlyUsed = session;
			}
		}
		
		leastRecentlyUsed.expireNow();
	}

	public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) {
		this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded;
	}

	public void setMaximumSessions(int maximumSessions) {
		Assert.isTrue(
				maximumSessions != 0,
				"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
		this.maximumSessions = maximumSessions;
	}

	public void setMessageSource(MessageSource messageSource) {
		Assert.notNull(messageSource, "messageSource cannot be null");
		this.messages = new MessageSourceAccessor(messageSource);
	}
}

 

package com.jaeson.springstudy.security;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;

public class MyRegisterSessionAuthenticationStrategy extends
		RegisterSessionAuthenticationStrategy {

	private SessionRegistry sessionRegistry;
	
	public MyRegisterSessionAuthenticationStrategy(SessionRegistry sessionRegistry) {
		super(sessionRegistry);
		this.sessionRegistry = sessionRegistry;
	}
	
	@Override
	public void onAuthentication(Authentication authentication,
			HttpServletRequest request, HttpServletResponse response) {
		
		if (sessionRegistry instanceof MySessionRegistryImpl)
			 sessionRegistry.registerNewSession(request.getSession().getId(), authentication);
		else
			sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
	}
}

 

 

<beans
	xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 
        http://www.springframework.org/schema/security 
		http://www.springframework.org/schema/security/spring-security.xsd ">

	<security:http pattern="/resources/**" security="none" />
	<security:http pattern="/*.html" security="none" />
	
	<!--  默认auto-config="false",设置auto-config="true"时,自动注册form-login、basic authentication、logout。
	默认use-expressions="true" 需要使用表达式hasRole('ROLE_USER') -->
	<security:http auto-config="true" use-expressions="false">
		<!-- 设置没有访问权限时转向的提示页面 -->
		<security:access-denied-handler error-page="/accessDenied.html" />
		<!-- 设置匿名用户可以访问的url -->
		<security:intercept-url pattern="/login*" access="IS_AUTHENTICATED_ANONYMOUSLY" />

		<!-- 设置相应角色可以访问的url -->
		<security:intercept-url pattern="/security/**" access="ROLE_ADMIN" />
		<security:intercept-url pattern="/**" access="ROLE_USER" />
		
		<!-- 设置自定义的登录页面和登录后的缺省home主页
		login-page不设置时,spring自动使用"/login"。
		default-target-url不设置时,登录成功后转向登录之前的请求url,如果没有则指向根目录"/"。
		always-use-default-target="true"登录成功后始终转向default-target-url。
		authentication-failure-url设置登录失败时转向的页面,如果不设置spring会自动转向"/login?error"。 -->
		<security:form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=1" 
				default-target-url="/" always-use-default-target="true" />
				
		<!-- 默认invalidate-session="true"在logout时使session失效,logout-success-url设置logout成功后转向的页面-->
		<security:logout logout-success-url="/logout.html" invalidate-session="true"/>

		
		<!-- data-source-ref="dataSource"使用数据库持久化remember me标记 -->
		<security:remember-me data-source-ref="dataSource" user-service-ref="userDetailsService" />
		
		<security:session-management session-authentication-strategy-ref="sessionAuthenticationStrategy"/>
 	</security:http>
 	
	<security:authentication-manager alias="authenticationManager">
		<security:authentication-provider user-service-ref="userDetailsService">
			<security:password-encoder ref="bcryptEncoder"/>
		</security:authentication-provider>
	</security:authentication-manager>
	
	<bean id="sessionAuthenticationStrategy"
    	class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
    	<constructor-arg>
    		<list>
    			<ref bean="concurrentSessionControlAuthenticationStrategy" />
    			<ref bean="sessionFixationProtectionStrategy" />
    			<ref bean="registerSessionAuthenticationStrategy" />
    		</list>
    	</constructor-arg>
    </bean>
	<bean id="sessionFixationProtectionStrategy" class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
	<bean id="concurrentSessionControlAuthenticationStrategy" class="com.jaeson.springstudy.security.MyConcurrentSessionControlAuthenticationStrategy">
    	<constructor-arg ref="sessionRegistry"/>
    	<property name="maximumSessions" value="1"/>
    	<property name="exceptionIfMaximumExceeded" value="true"/>
	</bean>
	<bean id="registerSessionAuthenticationStrategy" class="com.jaeson.springstudy.security.MyRegisterSessionAuthenticationStrategy">
    	<constructor-arg ref="sessionRegistry"/>
	</bean>
	<bean id="sessionRegistry" class="com.jaeson.springstudy.security.MySessionRegistryImpl" />

	<bean id="bcryptEncoder"
		class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

	<!-- 配置UserDetailsService实现,可以使用自定义的UserDetailsService实现获得数据库的用户信息UserDetails -->
	<bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
		<property name="dataSource" ref="dataSource" />
		<property name="usersByUsernameQuery" 
			value="SELECT username, password, enable FROM user WHERE username=?" />
		<property name="authoritiesByUsernameQuery" 
			value="SELECT u.username as username, r.rolename as rolename
					FROM user u
					JOIN user_group ug ON u.id=ug.user_id
					JOIN groups g ON ug.group_id=g.id
					JOIN group_role gr ON g.id=gr.group_id
					JOIN role r ON gr.role_id=r.id
					WHERE u.username=?" />
	</bean>

</beans>	

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值