SpringBoot+shiro+redis 一个账户只允许一处登录,强制用户下线

颜志学
2023-12-01

SpringBoot+shiro+redis 一个账户只允许一处登录,强制用户下线

概况

前期引入了redis来解决session共享,但并么有限制一个账户多人同时在线,且shiro本身没有带这个控制功能。在网上找了其他人的思路,但效果都有点不太好,于是自己写一下自己的项目实战。(第一次分享,希望各位大神多多指教)。

于是乎,利用redis 来记录一个用户的sessionID,如果一个用户存在多个sessionId,则获取队列末尾的sessionId,用sessionId查询对应的session,给session设置被踢出的标记。
当对应session,重新发起请求到后端,后端先判断session中是否有踢出标记,如果没有则正常操作,如果有踢出标记,则提示“已被强制下线”,同时session过期时间设置为0,立即过期清除。

redis中保存用户对应sessionId

先调好redis数据的存取

  RDeque<Serializable> deque = redissonClient.getDeque(ShiroUtil.getSessionUserName()));
   deque.push(sessionId);

shiro登录成功则校验用户session是否已存在

用户登录成功,在login方法追加校验是否唯一登录,或自定义注解或直接入侵

	//session 中key,是否被踢出
    public final static String FINAL_OUT_FLAG = "kick_out";
    private void checkUserSingle(Subject currentUser) {
        //获取当前用户的SessionID
        Serializable sessionId = currentUser.getSession().getId();
        //利用redisson  获取redis中的用户名与session列表,我们系统是name唯一,则以name为key,其他系统也可以使用id
        RDeque<Serializable> deque = redissonClient.getDeque(ShiroUtil.getSessionUserName()));
        //第一次登录则队列中为空,把自己存进去
        if (deque.isEmpty()) {
            deque.push(sessionId);
        }
        // 自己不是队列最后一个 也需要把自己添加进入队列,准备踢别人
        if (!sessionId.equals(deque.getLast())) {
            deque.push(sessionId);
        }

        //开始踢人,踢到只剩一个为止。(没有加锁,当前用户登录的同时,也可能同一个用户正在登录)
        while (deque.size() > 1) {
            try {
             	Serializable kickoutSessionId = deque.removeLast();
                //获取将要被踢出的sessionId的session对象
                Session kickOutSession = SecurityUtils.getSecurityManager().getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickOutSession != null) {
                    //设置session对象的kickout属性,true表示踢出了
                    kickOutSession.setAttribute(FINAL_OUT_FLAG, true);
                }
                //我没有处理异常
            } catch (Exception e) {
            }
        }
    }

注意此处没有直接将session立即过期,是希望给前端一个提示信息。因为我们系统没有消息推送服务,如果系统有推送消息服务,则推送提示消息给前端后,session可以立即过期。也就不需要后一步取拦截校验session是否已被踢出

shiroFilter中拦截校验session是否已被踢出

public class ShiroFilter extends FormAuthenticationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        // 判断请求是否是options请求
        String method = WebUtils.toHttp(request).getMethod();
        if (StringUtils.equalsIgnoreCase("OPTIONS", method)) {
            return Boolean.TRUE;
        }
        //判断是否已下线
        getKickOut(response);
        return super.isAccessAllowed(request, response, mappedValue);
    }
      @SneakyThrows
    private void getKickOut(ServletResponse response) {
        Subject subject = SecurityUtils.getSubject();
        //如果没有登录,直接进行登录的流程
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            return;
        }

        Session session = subject.getSession();
        //如果session没有被踢出去的标记 则不做动作
        if (ObjectUtil.isNull(session.getAttribute(FINAL_OUT_FLAG)) || !(Boolean) session.getAttribute(FINAL_OUT_FLAG)) {
            return;
        }

        //如果被踢出了,直接退出
        try {
            // 使用response响应流返回数据到前台(因前端需要接受json数据,注意前后端跨域问题)
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setContentType("application/json;charset=utf-8");
            //401 前端拦截所有401状态的返回,展示Message("账号已被强制下线!")即可
            httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            PrintWriter out = httpServletResponse.getWriter();

            ErrorResponse errorResponse = new ErrorResponse();
            errorResponse.setCode(ResultCode.UNAUTHORIZED.getCode());
            errorResponse.setCodeStr(ResultCode.UNAUTHORIZED.getMessage());
            errorResponse.setMessage("账号已被强制下线!");
            out.println(JSONUtil.toJsonStr(errorResponse));
            out.flush();
            out.close();

        } catch (Exception e) {
        } finally {
            //session 立即失效
            session.setTimeout(0);
        }
        throw new UnauthorizedException(ResultCode.UNAUTHORIZED, "账号已被强制下线");
    }
}

在shiroConfig初始化shiroFilterFactoryBean的时候,使用ShiroFilter来拦截。注意系统url拦截都需要配置authc,否则请求不进入isAccessAllowed();

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager,@Qualifier("sessionManager")  SessionManager sessionManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, Filter> map = Maps.newLinkedHashMapWithExpectedSize(3);
        ShiroFilter shiroFilter = new ShiroFilter();

        //限制跳转
        map.put("logout", shiroFilter);
        map.put("unauth", shiroFilter);
        map.put("authc", shiroFilter);
        shiroFilterFactoryBean.setFilters(map);
        //下面省略各种url的权限校验配置
}
 类似资料: