我想将每个用户的并发会话数限制为1。如果用户从第二个客户端/ IP登录,我想使他的上一个会话(如果有)无效,并为当前客户端创建一个新会话。因此,如果用户从第一个客户端发出另一个请求,则应该拒绝该用户访问并重定向。
我在Spring引导应用程序中使用Shiro。它是纯API服务器,而不是Web应用程序。前端和后端分开。
Shiro似乎没有开箱即用的会话限制支持。
我想知道应该在哪里执行这些操作?
目前我有自己的
public class AuthRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// Get user from db and return authentication info
}
}
我想知道添加相应逻辑的地方是否干净?还是应该在登录后创建上一个会话并创建第二个客户端的会话?
我认为这样做可能更有意义
public class AuthRealm extends AuthorizingRealm {
@Override
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
// Credentials verified
// Invalidate previous session
Subject subject = SecurityUtils.getSubject();
Session existingSession = subject.getSession(false);
if (existingSession != null) {
SecurityUtils.getSecurityManager().logout(subject);
existingSession.stop();
}
}
}
但是事实证明,Shiro创建了一个新主题,并与每个登录(而不是每个用户)关联了一个新会话。因此,subject = SecurityUtils.getSubject()
永远是全新的,检索相同的用户然后检索其会话是不可行的。有想法吗?
答案 0 :(得分:0)
经过大量研究,我发现最好的方法是编写自定义SecurityManager
:
public class UniquePrincipalSecurityManager extends DefaultWebSecurityManager {
private static final Logger logger = LoggerFactory.getLogger(AuthRealm.class);
@Override
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
// Verify credentials
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (logger.isInfoEnabled()) {
logger.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
// Check the subject's existing session and stop it if present
DefaultWebSessionManager sm = (DefaultWebSessionManager) getSessionManager();
User loggedInUser = (User)(info.getPrincipals().getPrimaryPrincipal());
for (Session session : sm.getSessionDAO().getActiveSessions()) {
SimplePrincipalCollection p = (SimplePrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
User sessionUser = null;
if (p != null) {
sessionUser = (User)(p.getPrimaryPrincipal());
}
if (sessionUser != null && loggedInUser.getId().equals(sessionUser.getId())) {
session.stop();
sm.getSessionDAO().delete(session);
}
}
// Create new session for current login
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
}
请注意,只有在验证凭据后才删除上一个会话。
使用这种方法,如果要将会话存储在另一个数据存储中,则还必须编写自己的会话管理器和sessionDAO。
请注意,我以前使用过带有Redis的Spring Session,但是使用此自定义安全管理器后,它不再起作用,因为不再将会话管理委托给HttpSession
。