springboot 实现登录互斥初版实现
简介:大致实现逻辑
具体效果是:一个用户只要在一台电脑上登录之后就会在 MySQL中保存一个 loginIp 字段,这样只要他没有退出,
没有session失效他就可以一直在当前的电脑上登录,此时其他人用此用户登录会提示,此用户已经登录不能重复登录,
如果用户主动退出,或session失效时就去更新用户 的login_ip 为 “”,退出和session失效的监控时间分别对应的
自定义session监听类的 onStop 和 onExpiration 方法。
环境:当前环境权限用的是shiro ,session 用 redis 管理
注意:这样有个问题是什么呢?就是项目在线上从新部署的时候那些已经登录的用户的状态已让保存在 MySQL中,
而且项目停止时没有事件能触发,所以就有了7步。
具体实现
1、数据库添加一个 login_ip字段,如果字段部位空,说明当前用户已登录
2、在登录的 controller中做逻辑,此时在session中 set 一个 userId,用于在session失效之后 SecurityUtil中已经取不出用户信息,只能从session中取userId 然后删除用户的 login_ip
public ResponseResult loginUser(@RequestBody @ApiParam(value = "用户名和密码", required = true) User user) {
logger.info("用户登录的入参信息:" + user);
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUsername(), user.getPassword());
Subject subject = SecurityUtils.getSubject();
User userInfo = null;
try {
//验证通过
subject.login(usernamePasswordToken);
userInfo = (User) subject.getPrincipal();
userInfo.setPassword("");
//1.判断是否允许登录
String loginIp = userService.getUserLoginIpById(userInfo.getId());
//1.1 当前登录用户ip 是否与 user表中loginIp一致 或为 空,一致允许登录,不一致不允许登录
if(StringUtils.isEmpty(loginIp) || loginIp.equals(subject.getSession().getHost())){
//登录成功时修改登录Ip
userService.updateLoginState(userInfo.getId(),subject.getSession().getHost());
subject.getSession().setAttribute("userId",userInfo.getId());
return ResponseResult.build(HttpServletResponse.SC_OK, "登录成功", userInfo, true);
}else{
return ResponseResult.build(HttpServletResponse.SC_NOT_ACCEPTABLE, "该账户已经在别处登录,您暂时无法使用此账号!", null, true);
}
} catch (Exception e) {
logger.error("登录失败", e);
return ResponseResult.build(HttpServletResponse.SC_UNAUTHORIZED, "登录失败", null, false);
}
}
3、配置redis缓存,将用户信息保存到redis中管理
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
@Resource(name="redisTemplateObj")
private RedisTemplate<String, Object> redisTemplate;
public RedisSessionDAO() {
super();
}
public RedisSessionDAO(RedisTemplate redisTemplate) {
super();
this.redisTemplate = redisTemplate;
}
/**
* 创建session,保存到数据库
*/
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
logger.debug("创建session:{}", session.getId());
redisTemplate.opsForValue().set(sessionId.toString(), session,1800L,TimeUnit.SECONDS);
return sessionId;
}
/**
* 获取session
*/
@Override
protected Session doReadSession(Serializable sessionId) {
logger.debug("获取session:{}", sessionId);
Session session = (Session) redisTemplate.opsForValue().get(sessionId.toString());
return session;
}
/**
* 更新session的最后一次访问时间
*/
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
logger.debug("获取session:{}", session.getId());
String key = session.getId().toString();
if (!redisTemplate.hasKey(key)) {
redisTemplate.opsForValue().set(key, session);
}
redisTemplate.expire(key, 1800L, TimeUnit.SECONDS);
Object object = redisTemplate.opsForValue().get(key);
}
/**
* 删除session
*/
@Override
protected void doDelete(Session session) {
logger.debug("删除session:{}", session.getId());
super.doDelete(session);
redisTemplate.delete(session.getId().toString());
}
}
4、配置一个 session的监听,为了在session失效的时候能将失效用户的 login_ip 清空,需要在 SessionManger 中配置自定义的session监听器 ShiroSessionListener.java ,本文中用的是 class 的配置方式,不是 xml,
@Configuration
@Order(2)
public class ShiroConfiguration {
/**
* 配置过滤器
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//自定义的拦截器
Map<String, Filter> filters = shiroFilter.getFilters();
filters.put("authc", myAuthenticationFilter());
shiroFilter.setFilters(filters);
//拦截器
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/api/user/login", "anon");
filterMap.put("/api/user/logOut", "anon");
filterMap.put("/druid/*", "anon");
filterMap.put("/api/**", "authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
@Bean
public FilterRegistrationBean registration(@Qualifier("myAuthenticationFilter") MyAuthenticationFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
/**
* 配置自定义的过滤器
*
* @return
*/
@Bean(name = "myAuthenticationFilter")
public MyAuthenticationFilter myAuthenticationFilter() {
return new MyAuthenticationFilter();
}
/**
* 配置核心安全事务管理器
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authRealm);
SecurityUtils.setSecurityManager(securityManager);
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 配置自定义的权限登录器
*/
@Bean(name = "authRealm")
public AuthRealm authRealm(@Qualifier("credentialsMatcher") CredentialsMatcher credentialsMatcher) {
AuthRealm authRealm = new AuthRealm();
authRealm.setCredentialsMatcher(credentialsMatcher);
return authRealm;
}
/**
* 配置自定义的密码比较器
*/
@Bean(name = "credentialsMatcher")
public CredentialsMatcher credentialsMatcher() {
return new CredentialsMatcher();
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager manager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(manager);
return advisor;
}
@Bean
public RedisSessionDAO redisSessionDAO(){
return new RedisSessionDAO();
}
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
//设置session失效时间
sessionManager.setGlobalSessionTimeout(30*60*1000L);
//删除过期的session
sessionManager.setDeleteInvalidSessions(true);
Collection<SessionListener> listeners = new ArrayList<SessionListener>();
listeners.add(new ShiroSessionListener());
sessionManager.setSessionListeners(listeners);
return sessionManager;
}
/**
* 设置cookie
*/
@Bean
public Cookie sessionIdCookie(){
Cookie sessionIdCookie=new SimpleCookie("STID");
sessionIdCookie.setMaxAge(-1);
sessionIdCookie.setHttpOnly(true);
return sessionIdCookie;
}
}
5、在写自定义的session的监听类的时候,要注意,用普通的 @Autowired 等方式不起作用,如果想获取什么对象只能从容器中取才行,注意 @WebListener 不可或缺
@Component
@WebListener
public class ShiroSessionListener extends SessionListenerAdapter {
Logger logger= LoggerFactory.getLogger(ShiroSessionListener.class);
@Override
public void onStart(Session session) {
logger.info("session创建:" + session.getId());
}
@Override
public void onStop(Session session) {
logger.info("session停止:" + session.getId());
//清空登录ip
SpringUtil.getBean(UserService.class).updateLoginState(Integer.parseInt(String.valueOf(session.getAttribute("userId"))),null);
//清空redis中sessionId
if(!StringUtils.isEmpty(String.valueOf(session.getId()))){
SpringUtil.getBean(RedisTemplate.class).delete(String.valueOf(session.getId()));
}
}
@Override
public void onExpiration(Session session) {
logger.info("session过期:" + session.getId());
//清空登录ip
SpringUtil.getBean(UserService.class).updateLoginState(Integer.parseInt(String.valueOf(session.getAttribute("userId"))),null);
//清空redis中sessionId
if(!StringUtils.isEmpty(String.valueOf(session.getId()))){
SpringUtil.getBean(RedisTemplate.class).delete(String.valueOf(session.getId()));
}
}
}
6、再附一个 我找的从SpringBoot 容器中回去对象的工具类
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通过name获取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通过class获取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
7、项目启动时的监听类
@Component
public class ClearLoginApplicationRunner implements ApplicationRunner {
Logger logger = LoggerFactory.getLogger(ClearLoginApplicationRunner.class);
@Autowired
UserService userService;
@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("after server started ...... invoking clearUserLoginIp interface ...");
//此处用来初始话user表的login_ip为""的逻辑
}
}