Shiro中的并发会话控制

时间:2019-01-31 06:32:09

标签: spring-boot shiro

我想将每个用户的并发会话数限制为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()永远是全新的,检索相同的用户然后检索其会话是不可行的。有想法吗?

1 个答案:

答案 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