公司内部管理系统使用 Shiro 做权限框架,Shiro 的缓存使用的是 Ehcache,一直存在个问题:单个账号可多处同时使用。出于安全考虑,这是不应该的,要实现的效果为避免单个账号多处保持登录状态。
解决方案挺简单,发生登录行为后使账号的上一次登录行为失效。在 Shiro 中我们可以通过 org.apache.shiro.session.mgt.eis.SessionDAO 实现会话管理,比如获取当前所有的活跃会话、更新单个会话信息。为了实现效果,我在现有的项目上做了些改造。
Shiro 配置
在 Shiro 的配置类中做以下操作。
注入 SessionDAO 实例:
1 2 3 4
| @Bean public SessionDAO sessionDAO() { return new MemorySessionDAO(); }
|
注入 SessionManager 实例:
1 2 3 4 5 6
| @Bean public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(sessionDAO()); return sessionManager; }
|
给 SecurityManager 设置 sessionManager 属性:
1 2 3 4 5 6 7 8
| @Bean(name = "securityManager") public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm()); securityManager.setCacheManager(cacheManager()); securityManager.setSessionManager(sessionManager()); return securityManager; }
|
发生登录行为后推送事件异步化处理
发生登录行为后推送登录事件去异步化处理,这里我觉得没有必要去做同步化处理,一是会使登录逻辑更加复杂化,二是多多少少会对登录速度有些影响。
准备使用 Guava 工具包下的 EventBus 来实现这个异步化处理逻辑,主要有两个核心类:LoginEventBusCenter(登录事件总线中心)、LoginEventObserver(登录事件观察者),下面附上它们的源码。
LoginEventBusCenter 用来注册/注销观察者、推送事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import com.google.common.eventbus.EventBus; import org.springframework.stereotype.Component;
@Component public class LoginEventBusCenter {
private final EventBus eventBus = new EventBus();
public void register(Object observer) { eventBus.register(observer); }
public void unregister(Object observer) { eventBus.unregister(observer); }
public void post(Object event) { eventBus.post(event); }
}
|
LoginEventObserverBo 作为一个中间业务对象,存储账号的会话 ID、用户 ID、用户名等信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString;
import java.io.Serializable;
@NoArgsConstructor @Getter @Setter @ToString public class LoginEventObserverBo implements Serializable { private static final long serialVersionUID = 1317296563624907994L;
private String sessionId;
private Integer userId;
private String username;
}
|
LoginEventObserver 用来进行登录后得处理,使用 Guava Cache 来暂存会话属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.eventbus.Subscribe; import org.apache.shiro.session.InvalidSessionException; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component public class LoginEventObserver {
private final Cache<String, LoginEventObserverBo> cache = CacheBuilder.newBuilder() .maximumSize(1000L) .expireAfterWrite(6L, TimeUnit.HOURS) .build();
@Autowired private SessionDAO sessionDAO;
@Subscribe public void handle(LoginEventObserverBo event) { if (event == null) { return; }
LoginEventObserverBo previous = cache.getIfPresent(event.getUsername()); if (previous != null) { try { sessionDAO.readSession(previous.getSessionId()).setTimeout(0L); } catch (InvalidSessionException ignored) { } }
cache.put(event.getUsername(), event); }
}
|
在登录事件总线中心注册观察者,这里我偷个懒,直接在 SpringBoot 的启动类注册:
1 2 3 4 5 6 7
| public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(App.class, args); LoginEventBusCenter loginEventBusCenter = context.getBean(LoginEventBusCenter.class); LoginEventObserver loginEventObserver = context.getBean(LoginEventObserver.class); loginEventBusCenter.register(loginEventObserver); }
|
发生登录行为后推送事件,部分代码如下:
1 2 3 4 5 6 7 8 9 10
| SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));
LoginEventObserverBo event = new LoginEventObserverBo(); event.setSessionId(SecurityUtils.getSubject().getSession().getId().toString()); UserBo user = getLoginUser(); event.setUsername(user.getUsername()); event.setUserId(user.getId()); loginEventBusCenter.post(event);
|