@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Resource
private SessionRegistry sessionRegistry;
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
/**
* 强制下线过滤器.<br/>
*
* @return
*
*/
@Bean
public ConcurrentSessionFilter concurrencyFilter() {
// 定义session失效后,重定向的url
ConcurrentSessionFilter concurrentSessionFilter = new ConcurrentSessionFilter(sessionRegistry(), "/kickout");
concurrentSessionFilter.setRedirectStrategy(autoRedirectStrategy());
return concurrentSessionFilter;
}
...
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// 设定强制下线自定义过滤器
http.addFilterAt(concurrencyFilter(), ConcurrentSessionFilter.class);
//session管理
//session失效后跳转
http.sessionManagement().invalidSessionUrl("/login");
//只允许一个用户登录,如果同一个账户两次登录,那么第一个账户将被踢下线,跳转到登录页面
http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");
}
}
UserInfo.java
public class UserInfo implements UserDetails {
...// 用户信息
@Override
public int hashCode() {
...// 自定义userInfo hashCode方法,session管理时会用做key
return new HashCodeBuilder(17, 37).append(getCompanyId()).append(getUserId()).append(getUsername())
.toHashCode();
}
@Override
public boolean equals(Object obj) {
...// 自定义equals方法
boolean isEqual = false;
if (obj != null && UserInfo.class.isAssignableFrom(obj.getClass())) {
UserInfo userInfo = (UserInfo) obj;
isEqual = new EqualsBuilder().append(getCompanyId(), userInfo.getCompanyId())
.append(getUserId(), userInfo.getUserId()).append(getUsername(), userInfo.getUsername()).isEquals();
}
return isEqual;
}
}
原理分析:
SessionRegistryImpl#registerNewSession(String sessionId, Object principal)
把用户的sessionId添加到principals Map中,key为登录用户principal(userInfo)的hashCode();
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);
}
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
sessionIds.put(sessionId,
new SessionInformation(principal, sessionId, new Date()));
// 根据principal(UserInfo)取得用户所有session
Set<String> sessionsUsedByPrincipal = principals.get(principal);
// 如果为空,新建,key为userInfo的hashCode,就是上面提到的自定义hashCode方法。
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);
}
}
ConcurrentSessionControlAuthenticationStrategy#onAuthentication 判断某个用户的session是否有超过设定连接数
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response) {
// 取得用户所有session
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
authentication.getPrincipal(), false);
int sessionCount = sessions.size();
// 在WebSecurityConfigurer中设定的最大session数
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
}
// 失效时间最长的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
SessionInformation leastRecentlyUsed = null;
// 判断session中哪个最后的访问时间最长,设为失效
for (SessionInformation session : sessions) {
if ((leastRecentlyUsed == null)
|| session.getLastRequest()
.before(leastRecentlyUsed.getLastRequest())) {
leastRecentlyUsed = session;
}
}
leastRecentlyUsed.expireNow();
}
在ConcurrentSessionFilter过滤器中
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
if (session != null) {
SessionInformation info = sessionRegistry.getSessionInformation(session
.getId());
if (info != null) {
// 如果session已经失效,重定向到设定好的expiredUrl中
if (info.isExpired()) {
// Expired - abort processing
if (logger.isDebugEnabled()) {
logger.debug("Requested session ID "
+ request.getRequestedSessionId() + " has expired.");
}
doLogout(request, response);
this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
else {
// Non-expired - update last request date/time
sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
}
chain.doFilter(request, response);
}