spring bootSSMShiro

shiro(7)-有状态认证-会话管理(单处登录-改进版)

2019-04-23  本文已影响139人  小胖学编程

目录
shiro(1)-架构
shiro(2)-有状态身份认证和无状态身份认证
shiro(3)-DelegatingFilterProxy的作用
shiro(4)-有状态认证-sessionManager会话
shiro(5)-有状态认证-Realm认证的实现
Shiro(6)-有状态认证-会话管理(整合Redis实现共享Session)
shiro(7)-有状态认证-会话管理(单处登录-改进版)

1. cookie和session的区别

1.1 cookie简介

1. 什么叫做cookie
Cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器。当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据去。这样,web资源处理的就是用户各自的数据了。
2. cookie的生命周期
那么cookie的有效期只在一次会话过程中有效,用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一次会话,当用户关闭浏览器,会话就结束了,此时cookie就会失效,

1.2 session简介

1. 什么叫做session
  Session是服务器端技术,利用这个技术,服务器在运行时可以为每一个用户的浏览器创建一个其独享的session对象,由于session为用户浏览器独享,所以用户在访问服务器的web资源时,可以把各自的数据放在各自的session中,当用户再去访问服务器中的其它web资源时,其它web资源再从用户各自的session中取出数据为用户服务。

1.3 sessionId和cookie的关系

客户端用cookie保存了sessionID,我们上面知道cookie默认的生命周期是一次会话,注意此时session在浏览器可能没有超时,cookie被销毁后,我们无法再找到sessionId,下次在请求服务器的时候,服务器会生成一个新的sessionId,保存在cookie中,返回客户端。

2. Shiro如何处理session

  1. 用户在登录成功之后,shiro会将用户的一些信息存储到sessionDao。保存形式为sessionID:session

  2. 用户在不同设备上登录成功之后(sessionId不同,但是session对象里面的"主键"相同),此时证明用户已经在其他设备上已登录。我们就可以处理我们的业务逻辑。

  3. 用户每次请求都会携带SESSIONID,服务器拦截到请求,判断是否要进行鉴权,如果进行鉴权的话,判断SESSION是否存在。

  4. 服务器将(旧的)session删除后,当用户再请求服务器时,服务端会抛出there is no session的异常,然后从新为请求建立一个新的session【返回到重新登录页面】,就像用户很长时间没有点击浏览器,shiro的定时器定时将失效的session清除的时候也抛出这个异常一样。不过这个对用户是透明的,对用户的体验没有影响。

3. 实现单处登录

3.1 XML配置

这里实现一个简易的xml配置。

本质上是通过session来完成用户认证,那么我们可以通过配置securityManagesessionManager来操纵session。

1. 加载sessionDao

<bean id="sessionDao" 
 class="org.apache.shiro.session.mgt.eis.MemorySessionDAO"/>
MemorySessionDAO实现的方法

2. 加载我们的sessionManage,它需要依赖我们的sessionDao

    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="sessionDAO" ref="sessionDao"/>
    </bean>

3. 加载securityManage

    <bean id="securityManager"
          class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="customRealm"></property>
        <!-- 注入缓存管理器 -->
        <property name="cacheManager" ref="cacheManager"></property>
        <property name="sessionManager" ref="sessionManager"></property>
    </bean>

3.2 代码配置

这里需要重写认证代码

  1. sessionDAO.getActiveSessions()顾名思义获取到所有存活的session对象。
  2. session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY)可以获取simpleAuthenticationInfo的第一个参数的值,其实就是principal对象,我们自定义的参数对象
  3. 根据session创建subject对象。
  4. 获取subject的principal对象(此处是自定义对象)。
  5. 判断principal对象里面的参数是否相同。
  6. 相同的话,可以删除session。两种方式:
    • session.setTimeout(0);设置session的失效时间;
    • sessionDAO.delete(session);直接删除session;
 @Autowired
    private SessionDAO sessionDAO;
 
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken aToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)aToken;
        String username = token.getUsername();
        //判断用户名是否存在
        User user = userService.selectUser(username);
        //声明一个user用来获取sessionDao中的user
        User userSession = null;
 
        if(user != null){
            //获取在线的session
            Collection<Session> sessionCollection = sessionDAO.getActiveSessions();
            for (Session session : sessionCollection){
                if(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) != null) {
                    //根据session build出一个subject
                    Subject subject = new Subject.Builder().session(session).buildSubject();
                    //拿到这个登陆的对象
                    userSession = (User) subject.getPrincipal();
                    //判断她的code和我现在登陆的code是否一致  (code是我在数据库里面设置一个标识码  采用uuid  保证唯一性 这里你可以id)
                    if (user.getCode().equals(userSession.getCode())) {
                        //两者一致的时候,设置这个session的失效时间 (0:立刻)
                        session.setTimeout(0);
                        break;
                    }
                }
            }
            AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
            return authenticationInfo;
        } else{
            throw new AuthenticationException("用户名错误!");
        }
    }

4. 单处登录-改进版

若是1亿用户登录,每次登录的用户都要循环1亿次判断是否异处登录吗?显然性不好,如何改进呢?

shiro整合redis中,将sessionId和session对象保存redis中后,我们将userId(业务字段)和sessionId也存储在redis中。

注意:在登录成功后,也将userId和sessionId保存到Redis中。

这样的话,我们可以快速的获取到sessionId和session对象了。

1. 在doGetAuthenticationInfo认证的时候,调用该方法,判断用户是否已登录

    private boolean isLogin(String userId, Session session) {
        //获取新请求的sessionId值
        Serializable httpSessionId = session.getId();
        //在Redis中获取到旧的sessionId的值
        String sessionId = jedisCluster.get(Constant.isLoginPrefix + userId);
        //如果没有存入到Redis中
        if(StringUtils.isBlank(sessionId)) {//未登录
            return false;
        }
        
        if(httpSessionId != null) {
            //格式化httpSessionId 的值
            httpSessionId = Base64.encode(SerializeUtil.serialize(Constant.REDIS_SHIRO_SESSION + httpSessionId));
            //若sessionId相等则是一个浏览器登录
            if(sessionId.equals(httpSessionId)) {//同一个浏览器二次登陆
                return false;
            }
        }
        //如果sessionId和session对应中,其实session为空
        String sessionObj = jedisCluster.get(sessionId);
        if(StringUtils.isBlank(sessionObj)) {//未登录
        //删除userId和sessionId对应关系
            jedisCluster.del(userId);
            return false;
        }else{//已登录
            //将旧的sessionId保存到新的session对象,后续删除该session对象。
            session.setAttribute("user.old.sessionId", sessionId);
            return true;
        }
    }

保存用户的token信息,然后抛出异常;

session.setAttribute("user.login.token", token);
throw new ConcurrentAccessException("用户已在其他地方登陆,是否继续登录");

2. 返回特殊的错误码
3. 前端收到特殊错误码后,要求用户是否执行再次登录,此时返回客户端一个新的sessionId。
4. 用户点击再次登录,请求携带新的sessionId到达Controller层

    @RequestMapping("/continueLogin")
    @ResponseBody
    public ResponseVo continueLogin() {
        ResponseVo vo = new ResponseVo(ResponseVo.FAIL, "登陆失败");
        //此时拿到的是新的SESSIONID的SESSION对象
        Session session = SecurityUtils.getSubject().getSession();
         //获取登录用户的账号密码对象
        Object tokenObj = session.getAttribute("user.login.token");

        if (tokenObj == null) {
            vo.setMessage("请重新点击登录!");
            return vo;
        }

        UsernamePasswordToken token = (UsernamePasswordTokenEx) tokenObj;
        
        if (StringUtils.isBlank(token.getUsername()) || token.getPassword() == null) {
            vo.setMessage("用户名或密码不能为空");
            return vo;
        }

        try {
            //内部封装了subject.login()方法
            if (loginService.login(token)) {
                //再次验证成功之后,删除已登陆的sessionId
                Object oldSessionIdObj = session.getAttribute("user.old.sessionId");
                if (oldSessionIdObj != null) {
                    String oldSessionId = String.valueOf(oldSessionIdObj);
                    String newSessionId = Base64.encode(SerializeUtil.serialize(Constant.REDIS_SHIRO_SESSION + session.getId()));
                    if (!StringUtils.isBlank(oldSessionId) && !oldSessionId.equals(newSessionId)) {
                         //删除Redis中的sessionId和就是删除Session中的SessionId
                        jedisCluster.del(oldSessionId);
                        logger.info("删除已登陆Session【{}】成功", oldSessionId);
                    }
                }
                vo = new ResponseVo(200, "登陆成功");
                return vo;
            }
      catch (Exception e) {
            vo.setMessage("登录失败!");
            logger.error("登陆失败", e);
        }
        return vo;
    }

需要注意的是,因为redis整合了session,session最终保存在Redis中,而不是服务器内存中。

文章参考:
1. shiro账号安全(每一个账号只能同时存在于一台设备上)
2. cookie和session的区别
3. 用户Cookie和会话Session、SessionId的关系

上一篇下一篇

猜你喜欢

热点阅读