springboot + shiro + redis管理session的时候更新身份信息无效

最近在做项目的时候遇到需要管理活跃session的需求。框架是用的SpringBoot,做了redis的session共享,并重写了shiro的sessionDao,代码如下:

RedisSessionDao.java

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;


/**
 * 重写了shiro的sessionDao,可以用于redis全局共享session
 * @author Mr.Tao
 *
 */
public class RedisSessionDao extends AbstractSessionDAO {
    // Session超时时间,单位为毫秒
    private long expireTime = 120000;
    private static final String KEY_PREFIX = "shiro_session:";
    private RedisTemplate<String,Session> redisTemplate;
    
    public RedisSessionDao() {
        super();
    }

    public RedisSessionDao(long expireTime, RedisTemplate<String,Session> redisTemplate) {
        super();
        this.expireTime = expireTime;
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        this.redisTemplate = redisTemplate;
    }

    @Override 
    public void update(Session session) throws UnknownSessionException {
        if (session == null || session.getId() == null) {
            return;
        }
        session.setTimeout(expireTime);
        redisTemplate.opsForValue().set(getKey(session.getId()), session, expireTime, TimeUnit.MILLISECONDS);
    }

    @Override // 删除session
    public void delete(Session session) {
        if (null == session) {
            return;
        }
        redisTemplate.opsForValue().getOperations().delete(getKey(session.getId()));
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<String> keys = redisTemplate.keys(KEY_PREFIX+"*");
        Set<Session> sessions = new HashSet<Session>(keys.size());
        for (Serializable key : keys) {
            sessions.add(redisTemplate.opsForValue().get(key));
        }
        return sessions;
    }

    @Override // 加入session
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        redisTemplate.opsForValue().set(getKey(session.getId()), session, expireTime, TimeUnit.MILLISECONDS);
        return sessionId;
    }

    @Override // 读取session
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            return null;
        }
        Session session = redisTemplate.opsForValue().get(getKey(sessionId));
        return session;
    }
    
    private String getKey(Serializable sessionId) {
        return KEY_PREFIX + sessionId;
    }

}

这样做了以后可以共享session,但是统计活跃session的时候出了问题,活跃session里面拿到的不是最新的user身份信息。问题复现如下:

在登录完成后以后,存入到Redis里面的session的信息:

{
  "lastAccessTime": 1607589832072,
  "expired": false,
  "host": "127.0.0.1",
  "attributes": {
    "org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY": true,
    "org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY": [
      {
        "lastChgPwdTime": 1606891249000,
        "lastLoginTime": 1607589548000,
        "loginId": 222,
        "loginCode": "ceshi",
        "staffName": "测试",
        "staffId": 10208
      }
    ]
  },
  "id": "4f097d02-624f-43c4-9333-3777dee638b1",
  "startTimestamp": 1607589832072,
  "timeout": 900000
}

然后我重新去set了user的ip信息。

LoginController.java

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(req.getUserName(), req.getPassword());
        try {
            subject.login(token);
            User user = (User)subject.getPrincipal();
            log.info("用户{}登陆成功", user.getStaffName());
            String loginIp = HttpUtil.getIpAddress(request);
            // 写入用户登录IP
            user.setLoginIp(loginIp);
            // 在这里重新去更新身份信息
            UserUtil.setUser(user);
        } catch (IncorrectCredentialsException e) {
            // 密码错误,记录当日错误次数
        } catch (LockedAccountException e) {
            // 账号被锁定
        } catch (Exception e) {
            // 其他异常
        }

 

并且更新了身份信息,更新身份信息的代码是网上找的,如下:

UserUtil.java

	    public static void setUser(User userInfo) {
	        Subject subject = SecurityUtils.getSubject();
	        PrincipalCollection principals = subject.getPrincipals();
	        //realName认证信息的key,对应的value就是认证的user对象
	        String realName= principals.getRealmNames().iterator().next();
	        //创建一个PrincipalCollection对象,userInfo是更新后的user对象
	        PrincipalCollection newPrincipalCollection = new SimplePrincipalCollection(userInfo, realName);
	        //调用subject的runAs方法,把新的PrincipalCollection放到session里面
	        subject.runAs(newPrincipalCollection);
	    }

问题就在这里,用这种方法更新了身份信息后,session的信息如下:

{
  "lastAccessTime": 1607589832072,
  "expired": false,
  "host": "127.0.0.1",
  "attributes": {
    "org.apache.shiro.subject.support.DelegatingSubject.RUN_AS_PRINCIPALS_SESSION_KEY": [
      [
        {
          "lastChgPwdTime": 1606891249000,
          "lastLoginTime": 1607589548000,
          "loginId": 222,
          "loginIp": "127.0.1.1",
          "loginCode": "ceshi",
          "staffName": "测试",
          "staffId": 10208
        }
      ]
    ],
    "org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY": true,
    "org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY": [
      {
        "lastChgPwdTime": 1606891249000,
        "lastLoginTime": 1607589548000,
        "loginId": 222,
        "loginCode": "ceshi",
        "staffName": "测试",
        "staffId": 10208
      }
    ]
  },
  "id": "4f097d02-624f-43c4-9333-3777dee638b1",
  "startTimestamp": 1607589832072,
  "timeout": 900000
}

可以看到虽然身份信息已经更新进去了,但是更新以后新加了一个key,叫做org.apache.shiro.subject.support.DelegatingSubject.RUN_AS_PRINCIPALS_SESSION_KEY并且该元素是一个身份信息的集合对象,而这个跟我们之前的身份信息org.apache.shiro.subject.support.DelegatingSubject.RUN_AS_PRINCIPALS_SESSION_KEY并不是一个key,使用这种方法去更新身份信息会导致统计活跃session失效。

统计活跃session的代码如下

public static List<User> getOnlineUsers(){ 
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) securityManager.getSessionManager();
        Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();
        List<User> users = new ArrayList<>();
        for (Session session : sessions) {
            // 判断用是否登录
            SimplePrincipalCollection simplePrincipalCollection = (SimplePrincipalCollection) session
                    .getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (simplePrincipalCollection != null) {
                User user = (User) simplePrincipalCollection.getPrimaryPrincipal();
                users.add(user);
            }
        }
        return users;
}

这里主要是从session里面去取用户信息,这里用的key为DefaultSubjectContext.PRINCIPALS_SESSION_KEY,这个常量的值为org.apache.shiro.subject.support.DelegatingSubject.RUN_AS_PRINCIPALS_SESSION_KEY而非使用subject.runAs方法所更新的key。

通过追踪源码可以发现

Subject实现类DelegatingSubject的getPrincipal方法关键代码如下:

DelegatingSubject.class

    /**
     * @see Subject#getPrincipal()
     */
    public Object getPrincipal() {
        return getPrimaryPrincipal(getPrincipals());
    }

    public PrincipalCollection getPrincipals() {
        List<PrincipalCollection> runAsPrincipals = getRunAsPrincipalsStack();
        return CollectionUtils.isEmpty(runAsPrincipals) ? this.principals : runAsPrincipals.get(0);
    }

    @SuppressWarnings("unchecked")
    private List<PrincipalCollection> getRunAsPrincipalsStack() {
        Session session = getSession(false);
        if (session != null) {
            return (List<PrincipalCollection>) session.getAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
        }
        return null;
    }

这里是优先通过RUN_AS_PRINCIPALS_SESSION_KEY去拿subject.runAs方法存储的元素,如果没有获取到,才会用PRINCIPALS_SESSION_KEY里的身份信息。所以一旦调用了subject.runAs方法后PRINCIPALS_SESSION_KEY里面的信息就等于无效了。而RUN_AS_PRINCIPALS_SESSION_KEY是DelegatingSubject类私有的常量,我们并不能通过去更换key来获取身份信息。

解决的方法是在更新身份信息的时候,不能调用subject.runAs,直接通过session去更新PRINCIPALS_SESSION_KEY里面的身份信息,解决代码如下:

	    public static void setUser(User userInfo) {
	        Subject subject = SecurityUtils.getSubject();
	        PrincipalCollection principals = subject.getPrincipals();
	        //realName认证信息的key,对应的value就是认证的user对象
	        String realName= principals.getRealmNames().iterator().next();
	        //创建一个PrincipalCollection对象,userInfo是更新后的user对象
	        PrincipalCollection newPrincipalCollection = new SimplePrincipalCollection(userInfo, realName);
	        //调用subject的runAs方法,把新的PrincipalCollection放到session里面
	        subject.getSession().setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, newPrincipalCollection);
	    }

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值