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

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的权限校验配置
}
  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot一个用于快速开发Java应用程序的开源框架,Shiro一个强大且易于使用的Java安全框架,Redis一个开源的内存数据库。结合使用这些技术可以实现单点登录功能。 在Spring Boot中使用Shiro来处理认证和授权,可以通过配置Shiro的Realm来实现用户登录认证和权限控制。将用户的信息存储在Redis中,利用Redis的持久化特性来实现用户登录状态的共享和存储。 首先,在Spring Boot项目的配置文件中配置Redis的连接信息,以便连接到Redis数据库。 然后,创建一个自定义的Shiro的Realm,在其中重写认证和授权的方法。在认证方法中,将用户登录信息存储到Redis中,以便其他服务可以进行验证。在授权方法中,根据用户的角色和权限进行相应的授权操作。 接着,在Spring Boot项目的配置类中配置Shiro的相关设置,包括Realm、Session管理器、Cookie管理器等。 最后,可以在Controller层中使用Shiro的注解来标记需要进行认证和授权的接口,以确保只有登录后且具备相应权限的用户才能访问这些接口。 总的来说,通过使用Spring BootShiroRedis的组合,可以实现单点登录的功能。用户登录后,将登录信息存储到Redis中,其他服务可以通过验证Redis中的数据来判断用户登录状态。同时,Shiro提供了强大的认证和授权功能,可以确保只有具备相应权限的用户才能访问受保护的接口。这些功能的具体实现可以通过深入研究Spring BootShiroRedis的源码来了解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值