最近項目中使用了shiro作權限管理。而後加了spring sessioin作集羣session控制(簡單),並無使用shiro redis管理session。前端
因爲以前併發登陸是用的spring security 。本項目中沒有。java
查看了 security和spring session的源碼發現使用過濾器和session過時字段實現的。至關於解析spring security session管理源碼了。web
故將源碼重寫了勉強實現了。記錄一下。redis
步驟:1.登陸經過用戶名這個惟一標識 上redis裏經過index檢索當前全部的符合相同用戶名的sessionspring
2.校驗獲取的session是否過時,根據條件判斷是否知足併發條件。將符合條件的session的expire字段設置爲過時 -即true。apache
3.使用過濾器攔截當前session判斷是否併發過時。--應該將過濾器放入shiro過濾鏈中。json
推薦本身用緩存等第三方庫保存惟一標識校驗。緩存
第一步 系列代碼session
package com.xxx.xxx.framework.common.session.registry; import java.io.Serializable; import java.util.Date; import lombok.Data; import org.springframework.util.Assert; /** * * ClassName : FastSessionInformation <br> * Description : session記錄--- <br> * Create Time : 2019年2月23日 <br> * 參考 spring security SessionInformation * */ @Data public class FastSessionInformation implements Serializable { /** TODO */ private static final long serialVersionUID = -2078977003038133602L; private Date lastRequest; private final Object principal; private final String sessionId; private boolean expired = false; // ~ Constructors // =================================================================================================== public FastSessionInformation(Object principal, String sessionId, Date lastRequest) { Assert.notNull(principal, "Principal required"); Assert.hasText(sessionId, "SessionId required"); Assert.notNull(lastRequest, "LastRequest required"); this.principal = principal; this.sessionId = sessionId; this.lastRequest = lastRequest; } // ~ Methods // ======================================================================================================== public void expireNow() { this.expired = true; } public void refreshLastRequest() { this.lastRequest = new Date(); } }
package com.xxx.xxx.framework.common.session.registry; import java.util.List; public interface FastSessionRegistry { public abstract List<FastSessionInformation> getAllPrincipals(); public abstract List<FastSessionInformation> getAllSessions(String paramObject, boolean paramBoolean); public abstract FastSessionInformation getSessionInformation(String paramString); public abstract void refreshLastRequest(String paramString); public abstract void registerNewSession(String paramString, Object paramObject); public abstract void removeSessionInformation(String paramString); }
package com.xxx.xxx.framework.common.session.registry; import java.util.Date; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; import org.springframework.session.SessionRepository; public class FastSpringSessionBackedSessionInformation<S extends Session> extends FastSessionInformation{ /** TODO */ private static final long serialVersionUID = 7021616588097878426L; static final String EXPIRED_ATTR = FastSpringSessionBackedSessionInformation.class .getName() + ".EXPIRED"; private static final Log logger = LogFactory .getLog(FastSpringSessionBackedSessionInformation.class); private final SessionRepository<S> sessionRepository; FastSpringSessionBackedSessionInformation(S session,SessionRepository<S> sessionRepository) { super(resolvePrincipal(session), session.getId(),Date.from(session.getLastAccessedTime())); this.sessionRepository = sessionRepository; Boolean expired = session.getAttribute(EXPIRED_ATTR); if (Boolean.TRUE.equals(expired)) { super.expireNow(); } } /** * Tries to determine the principal's name from the given Session. * * @param session the session * @return the principal's name, or empty String if it couldn't be determined */ private static String resolvePrincipal(Session session) { String principalName = session .getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME); if (principalName != null) { return principalName; } return ""; } @Override public void expireNow() { if (logger.isDebugEnabled()) { logger.debug("Expiring session " + getSessionId() + " for user '" + getPrincipal() + "', presumably because maximum allowed concurrent " + "sessions was exceeded"); } super.expireNow(); S session = this.sessionRepository.findById(getSessionId()); if (session != null) { session.setAttribute(EXPIRED_ATTR, Boolean.TRUE); this.sessionRepository.save(session); } else { logger.info("Could not find Session with id " + getSessionId() + " to mark as expired"); } } }
package com.xxx.xxx.framework.common.session.registry; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; import org.springframework.util.Assert; public class FastSpringSessionBackedSessionRegistry<S extends Session> implements FastSessionRegistry { private final FindByIndexNameSessionRepository<S> sessionRepository; public FastSpringSessionBackedSessionRegistry( FindByIndexNameSessionRepository<S> sessionRepository) { Assert.notNull(sessionRepository, "sessionRepository cannot be null"); this.sessionRepository = sessionRepository; } @Override public List<FastSessionInformation> getAllPrincipals() { throw new UnsupportedOperationException("SpringSessionBackedSessionRegistry does " + "not support retrieving all principals, since Spring Session provides " + "no way to obtain that information"); } @Override public List<FastSessionInformation> getAllSessions(String principal, boolean includeExpiredSessions) { Collection<S> sessions = this.sessionRepository.findByIndexNameAndIndexValue( FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal).values(); List<FastSessionInformation> infos = new ArrayList<>(); for (S session : sessions) { if (includeExpiredSessions || !Boolean.TRUE.equals(session .getAttribute(FastSpringSessionBackedSessionInformation.EXPIRED_ATTR))) { infos.add(new FastSpringSessionBackedSessionInformation<>(session, this.sessionRepository)); } } return infos; } @Override public FastSessionInformation getSessionInformation(String sessionId) { S session = this.sessionRepository.findById(sessionId); if (session != null) { return new FastSpringSessionBackedSessionInformation<>(session, this.sessionRepository); } return null; } /* * This is a no-op, as we don't administer sessions ourselves. */ @Override public void refreshLastRequest(String sessionId) { } /* * This is a no-op, as we don't administer sessions ourselves. */ @Override public void registerNewSession(String sessionId, Object principal) { } /* * This is a no-op, as we don't administer sessions ourselves. */ @Override public void removeSessionInformation(String sessionId) { } }
以上代碼 參考 重寫 spring security+spring session 併發登陸部分。能夠上 spring session官網查看。這部分是使session過時部分。併發
@Autowired
private FastSessionAuthenticationStrategy fastConcurrentSessionStrategy;
fastConcurrentSessionStrategy.onAuthentication(user.getAccount(), req, res);
在登陸方法中調用如上代碼便可將同用戶名的已登陸session標記爲過時了。我用的惟一標識是用戶名 ,頁能夠用別的 對象都行 看重寫代碼傳啥都行。
第二步 過濾器攔截 實現併發操做。提示 當前用戶已在其餘地方登陸
@Bean public FilterRegistrationBean concurrentSessionFilterRegistration(FastSessionRegistry sessionRegistry) { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new ConcurrentSessionFilter(sessionRegistry)); registration.addUrlPatterns("/*"); registration.setName("concurrentSessionFilter"); registration.setOrder(Integer.MAX_VALUE-2); return registration; } spring boot本身配置過濾器 記得啓動順序不能比shrio低
過濾器代碼 純copy spring security
import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; import com.asdc.fast.framework.common.session.registry.FastSessionInformation; import com.asdc.fast.framework.common.session.registry.FastSessionRegistry; import com.asdc.fast.framework.common.utils.HttpContextUtils; import com.asdc.fast.framework.common.utils.R; import com.google.gson.Gson; public class ConcurrentSessionFilter extends GenericFilterBean { private final FastSessionRegistry fastSessionRegistry; public ConcurrentSessionFilter(FastSessionRegistry fastSessionRegistry) { this.fastSessionRegistry=fastSessionRegistry; } @Override public void afterPropertiesSet() { Assert.notNull(fastSessionRegistry, "FastSessionRegistry required"); } 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) { FastSessionInformation info = fastSessionRegistry.getSessionInformation(session .getId()); if (info != null) { if (info.isExpired()) { // Expired - abort processing if (logger.isDebugEnabled()) { logger.debug("Requested session ID " + request.getRequestedSessionId() + " has expired."); } //doLogout(request, response); response.setContentType("application/json;charset=utf-8"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); String json = new Gson().toJson(R.error(455, "當前用戶已其餘地方登陸"));//給前端一個特定返回錯誤碼規定併發操做--455。 response.getWriter().print(json); //this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response)); return; } else { // Non-expired - update last request date/time fastSessionRegistry.refreshLastRequest(info.getSessionId()); } } } chain.doFilter(request, response); } /* private void doLogout(HttpServletRequest request, HttpServletResponse response) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); this.handlers.logout(request, response, auth); } public void setLogoutHandlers(LogoutHandler[] handlers) { this.handlers = new CompositeLogoutHandler(handlers); }*/ /** * A {@link SessionInformationExpiredStrategy} that writes an error message to the response body. * @since 4.2 */ /*private static final class ResponseBodySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy { @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { HttpServletResponse response = event.getResponse(); response.getWriter().print( "This session has been expired (possibly due to multiple concurrent " + "logins being attempted as the same user)."); response.flushBuffer(); } }*/ }
忘了第一步的關鍵操做 spring 註冊 策略
@Configuration/* @EnableRedisHttpSession*/ public class SpringSessionRedisConfig { /* @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } */ //redisfactory 使用 RedisConnectionFactory yaml默認提供的 @Bean public HttpSessionIdResolver httpSessionIdResolver() { HeaderHttpSessionIdResolver headerHttpSessionIdResolver = new HeaderHttpSessionIdResolver("token"); return headerHttpSessionIdResolver; } @Bean public FastSessionRegistry sessionRegistry(FindByIndexNameSessionRepository sessionRepository){ return new FastSpringSessionBackedSessionRegistry<Session>(sessionRepository); } @Bean public FastConcurrentSessionStrategy fastConcurrentSessionStrategy(FastSessionRegistry sessionRegistry){ return new FastConcurrentSessionStrategy(sessionRegistry); } }
基本上 就完成了一個簡單的併發登陸操做。