原理收藏-技术篇

SpringMVC + Shiro实现用户踢出功能

2020-03-01  本文已影响0人  视频怪物

整理自己的代码片段的时候, 想起来之前有一个需求: 实现同一个用户同时只能在一个地方登陆, 如果该用户在其他地方登陆, 踢出前一个登陆状态
项目中使用的是shiro做为权限控制框架, 对此需求进行了一些实现, 思路如下: shiro是利用一个个filter进行过滤请求的权限, 那么我就可以自定义一个filter在用户登陆之后, 判断当前的用户是否存在已经登陆的情况, 如果已经存在, 那么就踢出。


代码分析

  1. 存储用户当前登陆的状态, 这里其实可以用redis, 但是因为项目比较小, 我直接使用了内存Map来实现;
/**
 * 当前登录用户session的信息
 *
 * @author videomonster
 */
public class SessionCacheHolder {

    /**
     * 用户account, SessionId
     */
    public static Map<String, Serializable> loginSessionCache = Maps.newConcurrentMap();

    /**
     * session map
     * true: 踢出
     * false: 未踢出
     */
    public static Map<Serializable, Boolean> sessionStatusMap = Maps.newConcurrentMap();
}
  1. 定义一个KickoutSessionFilter, 这个filter继承Shiro提供的AccessControlFilter, 其中有两个方法需要我们复写实现, 一个是isAccessAllowed, 这个方法返回值是Boolean, 主要是判断逻辑是否能通过, 如果不通过, 就会调用第二个方法onAccessDenied来处理, 并且AccessControlFilter提供了getSubject方法, 可以通过当前的request和response获取当前主体(当前主体也可以通过Shiro提供的工具类SecurityUtils.getSubject()来获取);
/**
 * 踢出重复登录用户
 *
 * @author videomonster
 */
@Slf4j
@Component
public class KickoutSessionFilter extends AccessControlFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request,
                                      ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);
        //如果是相关目录或者是如果没有登录, 就直接return true
        if ((!subject.isAuthenticated() && !subject.isRemembered())) {
            return Boolean.TRUE;
        }
        // 获取当前登录的用户的相关信息
        Session session = subject.getSession();
        Serializable sessionId = session.getId();
        //  判断是否已经踢出
        Boolean kickout = SessionCacheHolder.sessionStatusMap.get(sessionId);
        if (null != kickout && kickout) {
            // 移除该session数据
            SessionCacheHolder.sessionStatusMap.remove(sessionId);
            return Boolean.FALSE;
        }
        // 获取mobileId
        User user = (User) subject.getPrincipal();
        String mobileId = user.getMobile();
        if (SessionCacheHolder.loginSessionCache.containsKey(mobileId)) {
            // 如果已经包含当前Session,并且是同一个用户,跳过。
            if (SessionCacheHolder.loginSessionCache.containsValue(sessionId)) {
                return Boolean.TRUE;
            }
            /*
             * 如果用户Id相同, Session值不相同
             * 1.获取到原来的session,并且标记为踢出。
             * 2.继续走
             */
            Serializable oldSessionId = SessionCacheHolder.loginSessionCache.get(mobileId);
            SessionCacheHolder.sessionStatusMap.put(oldSessionId, Boolean.TRUE);
            log.info("用户手机号: {}, 姓名: {} 登陆, 当前sessionId: {}, 踢出 session id: {}",
                    mobileId, user.getRealname(), sessionId, oldSessionId);
        }
        SessionCacheHolder.loginSessionCache.put(mobileId, sessionId);
        // 当前session标记为未被踢出
        SessionCacheHolder.sessionStatusMap.put(sessionId, Boolean.FALSE);
        return Boolean.TRUE;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request,
                                     ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        // 先退出该用户
        subject.logout();
        if (isAjaxRequest((HttpServletRequest) request)) {
            // 如果是ajax请求
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            Map<String, Object> rtn = Maps.newHashMap();
            rtn.put("code", 510);
            rtn.put("msg", "当前用户已在其他地方登陆, 请重新登录!");
            httpServletResponse.getWriter().write(JsonUtils.toJson(rtn));
        } else {
            // 重定向到指定位置
            WebUtils.issueRedirect(request, response, "/login.html");
        }
        return false;
    }

    private boolean isAjaxRequest(HttpServletRequest request) {
        String header = request.getHeader("X-Requested-With");
        return "XMLHttpRequest".equals(header);
    }
}
  1. 在shiro的配置文件中加入KickoutSessionFilter, 这里采用的是SpringMVC的XML配置模式。 需要注意的是, 因为这里只是想要实现判断当前用户是否已经在其他地方登陆, 所以我们需要把filter放到过滤链中执行真正进行鉴权的filter之前;
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- Shiro的核心安全接口,这个属性是必须的 -->
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.html"/>
    <!-- 登录成功后要跳转的连接 -->
    <property name="successUrl" value="/index.html"/>
    <property name="unauthorizedUrl" value="/404.html"/>
    <property name="filters">
      <util:map>
        <!-- 注册kickoutSessionFilter -->
        <entry key="kickout" value-ref="kickoutSessionFilter"/>
      </util:map>
    </property>
    <property name="filterChainDefinitions">
      <value>
        /resources/**=anon
        /favicon.ico=anon
        /login.html=anon
        /logout.html=anon
        /captcha.jpg=anon
        /login=anon
        /=anon
        <!-- 注册kickoutSessionFilter 到过滤链中, 并且放在真正鉴权处理的filter之前 -->
        /**=kickout,authc
      </value>
    </property>
  </bean>

效果展示

踢出用户

本文只是记录自己实现Shiro用户踢出的一种方式, 欢迎各位来指正和提出建议。

上一篇 下一篇

猜你喜欢

热点阅读