从零开始java安全权限框架篇(七):spring security整合Redis实现session共享

目录

 

一:spring security整合spring session实现session共享

二:代码

        1.springsecurity的配置文件

        2.自定义的session管理

三:获取到当前的所有用户以及踢出一个用户

四:用户锁定

1.我们先来看看锁定的时序图

2.流程解析

3.代码

(1)登录流程验证验证

(2)登录失败加锁

五:安全权限的总结

1.为什么要用Spring security

2.为什么要大量自定义和重写security的框架方法

3.主要的技术要点

4.主要实现功能


一:spring security整合spring session实现session共享

  其实这边坑还是还是很多了,我照着官网整合了半天,发现记住我的功能用不了(真的是springboot的几个版本都是用不了的)。然后就想跳过这个功能,毕竟之前也弄到过Redis上去了。那就去实现基于Redis的Session共享吧,结果整出来发现官网上一个说明心态崩了。

  我这边也尝试了用spring session整合来实现session共享,但是不知道为何很多Bug无法解决。比如记住我功能的无法实现,因为每一次都需要重写Cookie的机制很不好弄然后session共享也没办法获取到所有的用户等。最后,只能重写speing security的方法了具体实现如下:主要是就是Session共享。(完整代码块在最后)

二:代码

1.springsecurity的配置文件

package com.config.Seurity;






import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;

import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;





import org.springframework.stereotype.Repository;

import com.config.Seurity.hander.AjaxSuccessHander;
import com.config.Seurity.hander.AjaxfailHander;

import com.config.Seurity.permission.CustomPermissionEvaluator;
import com.config.Seurity.pwdEnder.MyPasswordEncoder;
import com.config.Seurity.repository.MyPersistentTokenRepository;
import com.config.Seurity.repository.MySessionRegistryImpl;
import com.config.Seurity.service.LoginService;







/**
 * spring security的配置
 * ClassName: SecurityConfig 
 * Function: 一句话描述功能. 
 * auth: monxz
 * date: 2019年8月28日 上午10:04:50 
 * @param 
 *
 *
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
		@Autowired
		private LoginService loginService;
		
		
		
		//静态文件夹忽略
		private String[] allowedRes= {"/static/**","/css/**","/js/**","/my/**","/img/**","/ajax/**","favicon.ico"};
		
		//不需要验证的
		private String[]  allowedUrl= {"/api/**","/user/user/current"};


		//登录执行的逻辑
		 @Autowired
		 public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
		      auth.userDetailsService(loginService).passwordEncoder(new MyPasswordEncoder());
		     
		  }

		 
		//配置信息
		 @Override
		    protected void configure(HttpSecurity http) throws Exception { 
			 	
		       
			 	//配置访问权限
		        http.authorizeRequests()
		        		//允许匿名访问(如api)
		                .antMatchers(allowedUrl)
		                .permitAll()		                        
		                //其他地址的访问均需验证权限
		                .anyRequest()
		                .authenticated();
		        		
		        
		         //配置登录以及成功失败的处理方式
		         http.formLogin()		               
		                //指定登录页是"/view/login"   
		                .loginPage("/view/login").permitAll()    //
		                
		                
		                //ajax方式登录		               
		                .successHandler(new AjaxSuccessHander())
		                .failureHandler(new AjaxfailHander())
		                .loginProcessingUrl("/login")
		                .usernameParameter("username")  //ajax请求必须的
		                .passwordParameter("password");
		                
		                //form表单登录
//		                .defaultSuccessUrl("/view/index")       //登录成功后默认跳转到路径"
		               
		                
		         //注销 ,直接访问   ip:port/logout		
		         http .logout()
		                .logoutSuccessUrl("/view/login")          //退出登录后跳转到登录主界面"
		                .deleteCookies()						//有记住我功能,删除cookie
		                .permitAll();
		         	
		                
		         //记住我
		         http.rememberMe()
		         .tokenRepository(persistentTokenRepository())
		          		.tokenValiditySeconds(60*15)
		          		
		          	
		          		;
		          		

		         
		                //跨域以及其他的一些配置
		         http .csrf()
		         	  .disable()   // 关闭CSRF跨域
		              .headers()
		              .frameOptions()
		              .sameOrigin();  // 允许加载frame子菜单
		        
		         http.sessionManagement()
//		         	 .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
		         	 .sessionFixation()
		         	 .migrateSession()
		             .invalidSessionUrl("/view/login")
		             .maximumSessions(1)
		             .expiredUrl("/view/login")
		             .sessionRegistry(sessionRegistry())		             
		             ;
		       
		         

		    }
		 
		
		 
		 
		 //静态资源
		 @Override
		  public void configure(WebSecurity web) throws Exception {
			 			
		        // 设置拦截忽略文件夹,可以对静态资源放行
		        web.ignoring().antMatchers(allowedRes);
		   }
		 
//================================权限认证=======================================		 
		 
		 // 注入自定义url和权限验证器	     
		    @Bean
		    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
		        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
		        handler.setPermissionEvaluator(new CustomPermissionEvaluator());
		        return handler;
		    }
			
		
//==========================session管理====================================================		
		 
		  //==========================session管理====================================================		
			 
			 //session全局监听
			 @Bean
			 public HttpSessionEventPublisher httpSessionEventPublisher() {
			      return new HttpSessionEventPublisher();
			 }
			 

			
			 
			 @Bean
			  public SessionRegistry sessionRegistry() {
				  //自定义session管理
			      return new SessionRegistryImpl();
			  }
			
		 
	 
			 
			
		 
 
			
		 
		 
		 	
		 
//===================记住我功能的实现===============================		 


		 //记住我功能的Token存储在Redis
			@Bean
			public PersistentTokenRepository persistentTokenRepository() {		       
			     return new MyPersistentTokenRepository();
			 } 
		    
		 



}

 2.自定义的session管理

package com.config.Seurity.repository;


import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import com.alibaba.fastjson.JSONObject;
import com.config.redis.constant.redisConstant;
import com.config.redis.service.redisService;


import commons.json.JSON;
import commons.utils.DateUtils;



@Component
public class MySessionRegistryImpl implements SessionRegistry,
ApplicationListener{
	
	protected final Log logger = LogFactory.getLog(MySessionRegistryImpl.class);
	
	
	
	//====================================================
	@Autowired
	private redisService  redisService;	
	
	private String hash_key=redisConstant.security_session_principals_key;
	
	private long timeout=redisConstant.security_session_timeout;
	
	//=====================================================
	public void sessionIdsPut(String sessionId,SessionInformation  t) {
		String str=JSON.marshal(t);
		redisService.set(sessionId, str,timeout);
	}
	
	public void sessionIdsRemove(String sessionId) {
		redisService.remove(sessionId);
	}
	
	
	public Set principalsGet(Object principal){
		String str=JSON.marshal(principal);
		return  (Set) redisService.hmGet(hash_key, str);
	}
	
	public Set principalsPutIfAbsent(Object principal,Set  strs){
		Set res=new HashSet();
		if(principalsGet(principal) != null ) {
			res= principalsGet(principal);
		}
		String str=JSON.marshal(principal);
		redisService.hmSet(hash_key, str, strs,timeout);		
		return res;
	}
	
	public void principalsRemove(Object principal){
		String str=JSON.marshal(principal);
		redisService.remove(str);
	}
	
	
	
	
	
	
	
	

	//===================================================
	@Override
	public SessionInformation getSessionInformation(String sessionId) {
		String  str = redisService.get(sessionId);
		
		try {
			if(str!= null && str.trim().length()  != 0) {
				SessionInformation 	sf = jsonToEntity(str);
				return sf;
			}
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			
		}
		return null;
	
		
	}
	
	
	public List  principalsKeySet(){
		Map map=redisService.hmMap(hash_key);
		if(map== null) {
			map=new HashMap();
		}
		
		return new ArrayList(map.keySet());
	}
	
	
	
	public SessionInformation  jsonToEntity(String jsonStr){	
//		SessionInformation  sf=new SessionInformation(principal, sessionId, lastRequest)
		JSONObject ssEntity = com.alibaba.fastjson.JSON.parseObject(jsonStr);
		
		Date  lastRequest=DateUtils.timesToData(ssEntity.getString("lastRequest"));
		
		Object  principal=ssEntity.get("principal");
		
		String sessionId=ssEntity.getString("sessionId");
		
		SessionInformation  sf=new SessionInformation(principal, sessionId, lastRequest);
				
		return sf;
	}
	
	
	
	
	
	
	
	
	
	
	
	
	//==================================================

	@Override
	public List getAllPrincipals() {
		logger.info("==============>获取到当前所有的用户");
		return new ArrayList<>(principalsKeySet());
	}

	@Override
	public List getAllSessions(Object principal, boolean includeExpiredSessions) {
		logger.info("====================>更具当前用户获取到所有的session");
		final Set sessionsUsedByPrincipal = principalsGet(principal);

		if (sessionsUsedByPrincipal == null) {
			return Collections.emptyList();
		}

		List list = new ArrayList<>(
				sessionsUsedByPrincipal.size());

		for (String sessionId : sessionsUsedByPrincipal) {
			SessionInformation sessionInformation = getSessionInformation(sessionId);

			if (sessionInformation == null) {
				continue;
			}

			if (includeExpiredSessions || !sessionInformation.isExpired()) {
				list.add(sessionInformation);
			}
		}

		return list;
	}

	
	public void onApplicationEvent(SessionDestroyedEvent event) {
		String sessionId = event.getId();
		removeSessionInformation(sessionId);
	}
	
	@Override
	public void refreshLastRequest(String sessionId) {
		logger.info("====================>刷新请求");
		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 principal) {
		logger.info("====================>session注册");
		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);
		}

			
		sessionIdsPut(sessionId,
				new SessionInformation(principal, sessionId, new Date()));

		Set sessionsUsedByPrincipal = principalsGet(principal);

		if (sessionsUsedByPrincipal == null) {
			sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
			Set prevSessionsUsedByPrincipal = principalsPutIfAbsent(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) {
		logger.info("====================>提出童虎"+sessionId);
		Assert.hasText(sessionId, "SessionId required as per interface contract");

		SessionInformation info = getSessionInformation(sessionId);

		if (info == null) {
			return;
		}

		if (logger.isTraceEnabled()) {
			logger.debug("Removing session " + sessionId
					+ " from set of registered sessions");
		}

		sessionIdsRemove(sessionId);

		Set sessionsUsedByPrincipal = principalsGet(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");
			}
			principalsRemove(info.getPrincipal());
		}

		if (logger.isTraceEnabled()) {
			logger.trace("Sessions used by '" + info.getPrincipal() + "' : "
					+ sessionsUsedByPrincipal);
		}
		
	}

}

三:获取到当前的所有用户以及踢出一个用户



package com.moudle.user.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.catalina.mbeans.UserMBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;

import org.springframework.stereotype.Service;

import com.config.Seurity.model.CurrentUser;
import com.moudle.user.service.SessionService;

import commons.utils.DateUtils;

/**
 *
 * ClassName:SessionServiceImpl 
* Reason: TODO ADD REASON.
* Date: 2019年9月3日 下午3:25:32
* @author Owner * @version * @since JDK 1.8 * @see */ @Service public class SessionServiceImpl implements SessionService{ @Autowired private SessionRegistry sessionRegistry; //获取到所有的在线对象 private List getAllUser(){ List list=new ArrayList(); try { list = sessionRegistry.getAllPrincipals(); } catch (Exception e) { e.printStackTrace(); } return list; } @Override public List getAllLoginUser() { List objs=this.getAllUser(); return objs; } @Async @Override public String kickSessionByUser(String[] userNames) { StringBuffer noKickOut=new StringBuffer(); List objs=this.getAllUser(); for(Object obj:objs) { if(obj instanceof CurrentUser) { CurrentUser cou=(CurrentUser) obj; for(String userName:userNames) { if(cou.getUsername().equals(userName)) { // false代表不包含过期session List sessionInfos=sessionRegistry.getAllSessions(cou, false); if(sessionInfos != null && sessionInfos.size() > 0) { for(SessionInformation sessionInformation:sessionInfos) { sessionInformation.expireNow(); sessionRegistry.refreshLastRequest(sessionInformation.getSessionId()); // sessionRegistry.removeSessionInformation(sessionInformation.getSessionId()); } }else { noKickOut.append(cou.getUsername()+","); } } } } } return noKickOut.toString(); } }

 

四:用户锁定

1.我们先来看看锁定的时序图

从零开始java安全权限框架篇(七):spring security整合Redis实现session共享_第1张图片

2.流程解析

从上面可以看出,关于锁定的主要有2个:一个是在登录时验证用户是否被锁定,另一个是在登录失败记性加锁。

3.代码

(1)登录流程验证验证

package com.config.Seurity.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import org.springframework.stereotype.Service;

import com.config.Seurity.exception.UserException;
import com.config.Seurity.model.CurrentUser;
import com.config.redis.constant.redisConstant;
import com.config.redis.service.redisService;
import com.moudle.system.dao.MenuDao;
import com.moudle.system.model.Menu;
import com.moudle.user.model.User;
import com.moudle.user.service.SessionService;

import com.moudle.user.service.UserService;

import commons.utils.DateUtils;
import commons.utils.IpUtils;

/*
 * 自定义用户登录授权service
 */
@Service("userDetailsService")
public class LoginService implements  UserDetailsService{
	
	@Autowired
	private UserService  userService;

	@Autowired
	private MenuDao  menuDao;
	
	@Autowired
	private redisService redisService;
	
	@Autowired
	private HttpServletRequest  request;
	
	//登陆错误的Redis前缀
	private String errorPrex=redisConstant.error_login_key_pre;
	
	//登陆错误的超时时间
	private Long errorTimeOut=redisConstant.errot_login_timeout;
	
	//单位时间内最大错误登陆次数
	private Long errorCount=redisConstant.error_login_max_count;
	
	
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UserException {
		
		
		User user=new User();
		user.setUserName(username);
		List  userList = userService.login(user);	
		
		//验证错误登陆次数
		if(valiErrorLogin() != 0L ){	
			throw new UserException("当前账号已被锁定请于"+DateUtils.secondsToDateStr(valiErrorLogin())
					                           +"后重试!");
		}
		
		if (userList == null || userList.isEmpty()) {
			throw new UserException("该用户不存在!");
		}		
		User  currentUser=userList.get(0);
		
		
		
		//添加权限
		List  permsList=new ArrayList();
		permsList =currentUser.getPerms()==null?
				new ArrayList():currentUser.getPerms();
		
		if(currentUser.getRole().getRoleRange() == 0) {
			//超级管理员
			List  menuList=menuDao.findList(new Menu())==null 
					? new ArrayList():menuDao.findList(new Menu());
					
			for(Menu menu:menuList) {
				permsList.add(menu.getPerms());
			}		
		
		}		
		//授权
		Collection authorities = new ArrayList<>();		
		permsList.forEach(o->{
			if(o!=null && o.trim().length() !=0) {
				authorities.add(new SimpleGrantedAuthority(o));
			}		
		});			
		
		return new CurrentUser(currentUser, authorities);
	}

	
	//验证错误次数
	public Long  valiErrorLogin(){
		Integer  count = redisService.get(errorPrex+ IpUtils.replace(request, null));
		
		if(count == null ){
			return 0L;
		}
		
		if( count > errorCount){
			return redisService.getExpireTime(errorPrex+ IpUtils.replace(request, null));
		}
		
		return 0L;
	}
	
	
	
	
}

(2)登录失败加锁

  说明:只要这块就是调用service只能用spring的上下文获取,而不能用@Resource或者@Autowired来获取,主要就是

             RedisTemplate的初始化的时机。其他也就是RedisTemplate实现自增的一个方法

package com.config.Seurity.hander;

import java.io.IOException;
import java.io.PrintWriter;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import com.config.Seurity.exception.UserException;
import com.config.redis.constant.redisConstant;
import com.config.redis.service.redisService;
import com.config.redis.service.impl.RedisServiceImpl;

import commons.json.JSON;
import commons.result.DataResult;
import commons.utils.IpUtils;



/**
 * 登录失败的ajax
 * ClassName: AjaxSuccessHander 
 * Function: 一句话描述功能. 
 * auth: monxz
 * date: 2019年8月28日 下午3:58:29 
 *
 *
 */

@Component

public class AjaxfailHander extends SimpleUrlAuthenticationFailureHandler implements ApplicationContextAware{
	
	private  static ApplicationContext applicationContext;
	
	//redis存储登录错误的key的前缀
	private  String errorLoginpre=redisConstant.error_login_key_pre;
	
	//redis存储的超时间
	private Long timeout=redisConstant.errot_login_timeout;
	
	//足底啊的错误次数
	private Long maxCount=redisConstant.error_login_max_count;
	
	@Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {      										
		setErrorCount(request);
		String message = e.getMessage();      
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(JSON.marshal(DataResult.buildFail(message)));
    }
	
	
	public void setErrorCount(HttpServletRequest request){
        redisService  redisService=		 applicationContext.getBean(redisService.class);
        String key=errorLoginpre+IpUtils.replace(request, null);
     
        Integer  nowCount=redisService.get(key);
        if(nowCount < maxCount) {
        	 redisService.incr(key, timeout);    
        }
            
		
	}


	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		
		if(AjaxfailHander.applicationContext == null ){
			AjaxfailHander.applicationContext=applicationContext;
		}
		
	}
	
	
	
}

代码模块:这里我直接把项目上传了当然,这项目还不完整:https://pan.baidu.com/s/1sneGpr2xWx1IRe1VxDzDIA

五:安全权限的总结

到这里安全权限框架的一些内容基本就结束了,下面就做一些总结:

1.为什么要用Spring security

  答:最早接触的是Shiro,还是认为Shiro较Security要简单的。但是为什么还是用security,主要原因是Spring security可以无缝连

          接Spirng。一开始学习Cloud在Oauth2鉴权那里就是用的Security+oauht2,因此这里充分折腾了一番。这里给出security+

          aouth2的博客:oauth2认证详解,oauth2的授权+密码,oauth2的数据持久化。

2.为什么要大量自定义和重写security的框架方法

  答:应为在搭建这个项目时,就是为之前的Spring cloud来完成血肉的。后续这个项目一定是向Dubbo和Cloud方向走的。因此,目前项目的所有的模块一定是基于分布式的。因此,大量折腾共享数据到Redis上的。

3.主要的技术要点

  这里主要的技术要点是Spring security+spring session 。但是说实话,spring session就是为了实现分布式情况下负载均衡下的session一致,也就是相当于客户端和服务端的中间件。之前也有折腾spring session来共享session等信息,但是折腾了大半天参考了官网上的大量信息,发现session的功能还是有缺陷,所以还是基于security+redis实现分布式session共享。

4.主要实现功能

  (1)记住我功能:存储Redis实现15分钟内无需重复登录

  (2)登录锁定:ip锁定,3次错误登录导致24小时锁定

  (3)权限检验:后端权限校验,针对每一个访问的检验

  (4)按钮级别的显示:后端权限具体化每一个前端的按钮

  (5)获取所有在线用户:动态获取所有的在线用户

  (6)踢出用户:根据用户名踢出当前在线用户

  (7)单用户登录:后一个用户登录会踢出同名的用户

  (8)单点登录:Redis存储可以实现单点登录(后续完成!)

5.遗留的一个问题

   在踢出用户锁定用户登录时长,用户被踢出来的瞬间会报500页面,然后在返回登录界面

你可能感兴趣的:(从零开始)