Shiro 中的并发会话控制
Concurrent Session Control in Shiro
我想将并发会话数限制为每个用户 1 个。如果用户从第二个 client/IP 登录,我想使他以前的会话(如果有的话)无效并为当前客户端创建一个新会话。因此,如果用户从第一个客户端发出另一个请求,他应该被拒绝访问并重定向。
我在 Spring 启动应用程序中使用 Shiro。它是一个纯粹的 API 服务器,而不是网络应用程序。前后端分离
似乎 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()
将始终是一个全新的,并且不可能检索同一个用户然后检索其会话。有什么想法吗?
经过大量研究,我发现最好的方法是编写自定义 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。
请注意,我之前使用 Spring 会话与 redis,但是使用这个自定义安全管理器,它不再起作用,因为会话管理将不再委托给 HttpSession
。
我想将并发会话数限制为每个用户 1 个。如果用户从第二个 client/IP 登录,我想使他以前的会话(如果有的话)无效并为当前客户端创建一个新会话。因此,如果用户从第一个客户端发出另一个请求,他应该被拒绝访问并重定向。
我在 Spring 启动应用程序中使用 Shiro。它是一个纯粹的 API 服务器,而不是网络应用程序。前后端分离
似乎 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()
将始终是一个全新的,并且不可能检索同一个用户然后检索其会话。有什么想法吗?
经过大量研究,我发现最好的方法是编写自定义 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。
请注意,我之前使用 Spring 会话与 redis,但是使用这个自定义安全管理器,它不再起作用,因为会话管理将不再委托给 HttpSession
。