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