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>