Spring Session 的原理

2022年 7月 15日 67点热度 0人点赞

file

引言

今天在写一个对外接口, 这个接口大致原理是在过滤器中通过 token 获取用户信息然后创建 session, 后续的流程就是 Controller -> Service -> Dao 了.

这次开发没有像之前那样愣头愣脑的, 我想了一下, 对方调用的时候是没有 session id 的, 也就是每次认证之后都会创建一个 session. 那这就可能存在一个大问题了, 假设调用次数非常多的话, 会创建茫茫多的 session, 可能会击垮系统.

所以我的看下我们系统中是如何使用 session 的.

Spring Session 探索

代码跟踪

第一件做的是就是断点 request 获取 session 的代码, 果然是有说法啊!

request.getSession()

file
request 获取 session 的断点

@SuppressWarnings("deprecation")
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerRequestFilter {
    public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class.getName();

    public static final int DEFAULT_ORDER = Integer.MIN_VALUE + 50;

    private final SessionRepository<S> sessionRepository;

    private ServletContext servletContext;

    private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();

file
创建 session

public void setAttribute(String name, Object value) {
    // 还挺讲究的, 设置 attr 之前先 checkState
    checkState();
    session.setAttribute(name, value);
}

checkState() 也很简单, 就是校验 invalidated 是否为 true.

private void checkState() {
    if(invalidated) {
        throw new IllegalStateException("The HttpSession has already be invalidated.");
    }
}

光从代码找还是有点难找的, 不过还是找到了:

file
持久化 session

file
存储 session 的 key 和过期时间

file
存储业务数据到 session 中

验证 redis 中的数据

上个 debug 的 session 的 key 是: spring:session:sessions:62359810-d2cb-4378-a619-e2c31bb8242c, 看上去是存了一个 hash 结构.

redis 中获取 hash 的命令是:

HGETALL hkey

执行一下:

127.0.0.1:6379> HGETALL spring:session:sessions:62359810-d2cb-4378-a619-e2c31bb8242c
1) "maxInactiveInterval"
2) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x01Q\x80"
3) "lastAccessedTime"
4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x82\x00:\x99\x0e"
5) "creationTime"
6) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x82\x00:\x99\x0e"

里面好像还是没有业务数据, 艹, 发现我傻逼了, 断点还没放开, redis 压根还没存这个业务数据.

再执行一遍:

127.0.0.1:6379> HGETALL spring:session:sessions:62359810-d2cb-4378-a619-e2c31bb8242c
 1) "creationTime"
 2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x82\x00:\x99\x0e"
 3) "lastAccessedTime"
 4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x82\x00D\x1f["
 5) "sessionAttr:HumanSession"
 6) "\xac\xed\x00\x05sr\x00#cn.com.xxx.base.bean.HumanSession\x8f\xbb\xb6d\xf6\x04\x8a\xd5\x02\x00\x1fD\x00\x0bcoordinateXD\x00\x0bcoordinateYZ\x00\tvalidFlagL\x00\x0fautoReceiveFlagt\x00\x13Ljava/lang/Integer;L\x00\x0ebrowserVersiont\x00\x12Ljava/lang/String;L\x00\ndataUnitIDq\x00~\x00\x01L\x00\afromCasq\x00~\x00\x01L\x00\tfromTokenq\x00~\x00\x01L\x00\thumanCodeq\x00~\x00\x02L\x00\ahumanIDq\x00~\x00\x01L\x00\thumanNameq\x00~\x00\x02L\x00\nhumanStyleq\x00~\x00\x02L\x00\ninvalidMsgq\x00~\x00\x02L\x00\x02ipq\x00~\x00\x02L\x00\rleaderLevelIDq\x00~\x00\x01L\x00\x05logIDq\x00~\x00\x01L\x00\tosVersionq\x00~\x00\x02L\x00\npatrolFlagq\x00~\x00\x01L\x00\bportraitq\x00~\x00\x02L\x00\bproxyUrlq\x00~\x00\x02L\x00\nregionCodeq\x00~\x00\x02L\x00\bregionIDq\x00~\x00\x01L\x00\nregionNameq\x00~\x00\x02L\x00\nregionTypeq\x00~\x00\x01L\x00\bserverIpq\x00~\x00\x02L\x00\x06targetq\x00~\x00\x02L\x00\ttelMobileq\x00~\x00\x02L\x00\aunionIDq\x00~\x00\x01L\x00\x06unitIDq\x00~\x00\x01L\x00\bunitNameq\x00~\x00\x02L\x00\buserNameq\x00~\x00\x02xp\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01pppsr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x00q\x00~\x00\x06t\x00\rwizdom:100433sq\x00~\x00\x04\x00\x01\x88Qt\x00\x05xxxxx\x00\bdarkbluept\x00\x0f192.168.213.161pppq\x00~\x00\x06t\x00\x00ppq\x00~\x00\x06psq\x00~\x00\x04\x00\x00\x00\x01t\x00\t127.0.0.1pt\x00\x0b17700000000sq\x00~\x00\x04\x00\x00\x02\xbesq\x00~\x00\x04\x00\x00\x00\x02t\x00\x0f\xe5\xb8\x82\xe7\x9b\x91\xe7\x9d\xa3\xe4\xb8\xad\xe5\xbf\x83t\x00\x05xxx"
 7) "maxInactiveInterval"
 8) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x01Q\x80"
 9) "sessionAttr:UnionAuthToken"
10) "\xac\xed\x00\x05t\x00$125ba47c-fe01-42b6-b5a2-8a87eb266ddc"

这回有了, 而且还很多.

过期时间如何设置呢?

其实在可以加一个过滤器, 像平常那样设置 session 的过期时间就行:

req.getSession().setMaxInactiveInterval(expireTime);
filterChain.doFilter(servletRequest, servletResponse);

session 存储的小结

Spring Session 对 JavaWeb 中的 session 进行了一层包装, 写业务时候的接口都保持不变, 但是底层的存储从 Tomcat 中的内存变成了 Redis, 而且用户还没有感知.

如果可以能用哨兵模式保证 Redis 的高可以, 感觉是不是就解决了分布式 Session 的问题.

Session 的 invalidate 实现

HttpSession session = request.getSession();
if(session != null){
    session.removeAttribute(/* here is your attr name*/);
    session.invalidate();
}

看来这里主要就是 session.invalidate() 了.

// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.
// HttpSessionWrapper#invalidate
public void invalidate() {
    checkState();
    this.invalidated = true;
    requestedSessionInvalidated = true;
    setCurrentSession(null);
    sessionRepository.delete(getId());
}

根据 id 删除 session:

// org.springframework.session.data.redis.RedisOperationsSessionRepository#delete
public void delete(String sessionId) {
    ExpiringSession session = getSession(sessionId, true);
    if(session == null) {
        return;
    }

    String key = getKey(sessionId);
    expirationPolicy.onDelete(session);

    // always delete they key since session may be null if just expired
    this.sessionRedisOperations.delete(key);
}

onDelete 中也删除数据的:

    public void onDelete(ExpiringSession session) {
        long toExpire = roundUpToNextMinute(expiresInMillis(session));
        String expireKey = getExpirationKey(toExpire);
        expirationRedisOperations.boundSetOps(expireKey).remove(session.getId());
    }

盗来的一张图

file
上图来自: https://www.cnblogs.com/jpfss/p/10894358.html

回到我的问题

第三方通过 token 调用接口创建会话的问题很简单, 调用完了之后就可以将 session invalidate 了.

哈哈

rainbow

这个人很懒,什么都没留下

文章评论