session共享的几种方案
方案一
不放在服务器应用中管理,放在第三方统一管理,如redis,数据库等,现在主流都是放在redis 因为redis高效qps最高能达到10万+
方案二
session 拷贝,集群情况某一台服务器session发生改变,通知其他服务器,这样会有个问题,如果集群实例太多,要通知太多的服务器,而且原子性也需要保证(所有服务要么一起成功要么一起失败)
方案三
粘滞会话,我第一家公司就是这样处理,根据用户id nginx都路由到一个实例
自定义
使用spring自带的实现
通过SpringSessionBackedSessionRegistry 通过关键字百度搜搜很多方案
自定义SessionRegistory
可以参考默认的org.springframework.security.core.session.SessionRegistryImpl 因为是基于应用的 sessionCRUD都是操作应用内数据结构我们
1.接口定义
public interface SessionRegistry { //获得所有会话sessionId List<Object> getAllPrincipals(); //获得指定用户的会话列表 List<SessionInformation> getAllSessions(Object userId, boolean var2); //获得指定sessionId的会话 SessionInformation getSessionInformation(String sessionId); //刷新访问时间 void refreshLastRequest(String var1); //注册会话 void registerNewSession(String sessionId, Object var2); //删除会话 void removeSessionInformation(String var1); }
2.实现
public class RedisSessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> { private RedisTemplate redisTemplate; //维护所有sessionId列表的zset score为过期时间 private final static String SESSION_LIST_KEY = "session:list"; //维护指定用户sessionId列表的zset score为过期时间 private final static String SESSION_USER_LIST_KEY = "session:list:userId:%s"; //存储sessionId对应的数据 private final static String SESSION_ITEM_KEY = "session:item:%s"; public RedisSessionRegistryImpl(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override public void onApplicationEvent(SessionDestroyedEvent sessionDestroyedEvent) { String sessionId = sessionDestroyedEvent.getId(); this.removeSessionInformation(sessionId); } /** * 获得所有会话 * * @return */ @Override public List<Object> getAllPrincipals() { Set<Object> sessionIds = redisTemplate.boundZSetOps(SESSION_LIST_KEY).range(0, -1); if (CollectionUtils.isEmpty(sessionIds)) { return null; } return sessionIds.stream().collect(Collectors.toList()); } /** * 获得指定用户列表的会话 * * @param includeExpiredSessions * @return */ @Override public List<SessionInformation> getAllSessions(Object userId, boolean includeExpiredSessions) { Set<String> sessionsUsedByPrincipal = redisTemplate.boundZSetOps(String.format(SESSION_USER_LIST_KEY, userId)).range(0, -1); if (sessionsUsedByPrincipal == null) { return Collections.emptyList(); } else { List<SessionInformation> list = new ArrayList(sessionsUsedByPrincipal.size()); Iterator var5 = sessionsUsedByPrincipal.iterator(); while (true) { SessionInformation sessionInformation; do { do { if (!var5.hasNext()) { return list; } String sessionId = (String) var5.next(); sessionInformation = this.getSessionInformation(sessionId); } while (sessionInformation == null); } while (!includeExpiredSessions && sessionInformation.isExpired()); list.add(sessionInformation); } } } /** * 根据sessionId获得会话 * * @param sessionId * @return */ @Override public SessionInformation getSessionInformation(String sessionId) { Object jsonValue = redisTemplate.opsForValue().get(String.format(SESSION_ITEM_KEY, sessionId)); if (StringUtils.isEmpty(jsonValue)) { return null; } JSONObject jsonObject= JSON.parseObject(jsonValue.toString()); SessionInformation sessionInformation=new SessionInformation(JSON.parseObject(jsonObject.getString("principal"), UserInfoDto.class),jsonObject.getString("sessionId"),jsonObject.getDate("lastRequest")); if(jsonObject.getBoolean("expired")){ sessionInformation.expireNow(); } return sessionInformation; } /** * 刷新session过期时间 * * @param sessionId */ @Override public void refreshLastRequest(String sessionId) { SessionInformation info = this.getSessionInformation(sessionId); if (info != null) { info.refreshLastRequest(); } registerNewSession(sessionId, info); } /** * 注册session * * @param sessionId * @param principal */ @Override public void registerNewSession(String sessionId, Object principal) { SessionInformation sessionInformation =new SessionInformation(principal, sessionId, new Date()); redisTemplate.opsForValue().set(String.format(SESSION_ITEM_KEY, sessionId), JSON.toJSONString(sessionInformation)); redisTemplate.boundZSetOps(SESSION_LIST_KEY).add(sessionId, System.currentTimeMillis() + 30000); String username; //因为第一次传入的是username 登录成功传入的是我们的UserDetails对象 if(principal instanceof UserDetails){ username=((UserDetails)principal).getUsername(); }else{ username=principal.toString(); } redisTemplate.boundZSetOps(String.format(SESSION_ITEM_KEY, username)).add(sessionId, System.currentTimeMillis() + 30000); } /** * 从会话中移除 * * @param sessionId */ @Override public void removeSessionInformation(String sessionId) { SessionInformation sessionInformation = getSessionInformation(sessionId); redisTemplate.delete(String.format(SESSION_ITEM_KEY, sessionId)); redisTemplate.boundZSetOps(SESSION_LIST_KEY).add(sessionId, System.currentTimeMillis() + 30000); if (sessionInformation != null) { redisTemplate.boundZSetOps(String.format(SESSION_ITEM_KEY, sessionInformation.getPrincipal())).add(sessionId, System.currentTimeMillis() + 30000); } } }
3.替换
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .rememberMe() .key("system") .and() .formLogin() .authenticationDetailsSource(new MyWebAuthenticationDetailsSource()) .usernameParameter("loginName") .passwordParameter("loginPassword") .defaultSuccessUrl("/hello") .failureForwardUrl("/loginFail") .failureUrl("/login.html") .permitAll()//不拦截 .and() .csrf()//记得关闭 .disable() .sessionManagement() .maximumSessions(1) .maxSessionsPreventsLogin(true) .sessionRegistry(new RedisSessionRegistryImpl(redisTemplate)); }
源码
回头看《spring-security使用-同一个账号只允许登录一次(五)》的源码
1.org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy#onAuthentication
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException { SessionAuthenticationStrategy delegate; //遍历delegateStrategies 调用onAuthentication方法 for(Iterator var4 = this.delegateStrategies.iterator(); var4.hasNext(); delegate.onAuthentication(authentication, request, response)) { delegate = (SessionAuthenticationStrategy)var4.next(); if (this.logger.isDebugEnabled()) { this.logger.debug("Delegating to " + delegate); } } }
2.org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy#onAuthentication
public class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategy { private final SessionRegistry sessionRegistry; public RegisterSessionAuthenticationStrategy(SessionRegistry sessionRegistry) { Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null"); this.sessionRegistry = sessionRegistry; } public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal()); } }