最近项目中使用了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); } }
基本上 就完成了一个简单的并发登陆操做。