美文网首页
记一次长连接导致的session不一致

记一次长连接导致的session不一致

作者: 小东班吉 | 来源:发表于2023-11-29 15:41 被阅读0次

背景

昨天遇到一个问题,用户登陆打印后台管理后,点击其他菜单会自动退出,跳转到登陆页面

排查

经过查看请求日志发现确实每次登陆后,再浏览其他页面时会自动跳转到登陆页。
回忆下项目中关于后台登陆的相关实现:

  1. 管理后台使用beego,基本上只使用了它的路由以及session管理
  2. 登陆相关有jwt和session,而我们管理后台登陆仍然使用的是session,所以jwt可以排除了
  3. session的实现包含了manage,store,provider 3个interface,以及store接口的不同实现对象,我们使用的是mysql存储。那一般访问session通过manage,找到provider,然后调用sessionStore的具体实现
  4. session是存储在数据库当中的,三个字段,分别是seesionId,sesssionData,sessionExpiry
  5. sessionId存储在cookie当中,在每次请求进来时会调用sessionRead,开启或者恢复上次会话,请求结束的时候会保存当前的会话以做下次请求恢复
  6. sessionId是存储在cookies当中的,每次请求会带上,用来操作session数据,CruSession在这里就是session store(beego为啥不统一用session manage来管理呢),源码如下:
func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    if BConfig.WebConfig.Session.SessionOn {  
        var err error  
        context.Input.CruSession, err = GlobalSessions.SessionStart(rw, r)  
        //...
        defer func() {  
           if context.Input.CruSession != nil {  
              context.Input.CruSession.SessionRelease(rw)  
           }  
        }()  
    }
}

分析:

  1. 登陆后到再跳转到登陆页的所有请求都有携带sessionId
  2. 登陆服务端没有任何错误出现,session存储访问也正常,过期时间等都正常。
  3. 通过1和2可以猜测可能有其他请求把session覆盖了或者清空了
  4. 我们有个sse的长连接,主要利用了sse服务端断开后,客户端会重新发起新的连接连接服务端的特性,实现了当服务端更新后,sse的error事件触发后再重新reload服务端页面。
  5. 查看代码sse的请求是不经过登陆的,在登陆前就会初始化并连接到服务端,登陆后页面跳转了,在跳转前组件卸载的时候会调用sse.close关闭长连接

看起来问题已经比较明了,sse引起的,一般我们登陆都是服务端存储session后告诉客户端,客户端接下来跳转页面,跳转页面时sse.close事件触发,服务端会收到客户端断开连接的通知,然后退出,代码如下。
客户端:

export default function useConnectionDetect() {
  useEffect(() => {
    const sse = new EventSource(`${endpoint}/sse`);
    let retries = 0;
    let down = false;

    sse.onopen = () => {
      console.log(`connection established. retry: ${retries}, down: ${down}`);
      retries = 0;
      if (down === true) {
        window.location.reload();
      }
    };

    sse.onerror = () => {
      if (retries < MAX_RETRIES) {
        console.log(`connection lost, retrying(${retries})...`);
        retries += 1;
        return;
      }
      withClient(({ context }) => {
        context.notification({
          title: '安全审计系统',
          content: '服务器连接失败,请检查网络连接或联系管理员',
        });
      });
      console.error('server is down');
      down = true;
    };

    return () => {
      console.log(`closing connection...`);
      sse.close();
    };
  }, []);
}

服务端:

for {  
    select {  
    case <-this.Ctx.ResponseWriter.CloseNotify():  
       debug.DebugThunk(func() {  
          logs.Debug("客户端主动关闭了 SSE 链接")  
       })  
       return  
    case <-timer.C:  
       logs.Debug("timeout")  
       return  
    default:  
       logs.Debug("send")  
       data := fmt.Sprintf("data: %s\nretry: %d\n\n", "pong!", retryInterval*1e3)  
       this.Ctx.ResponseWriter.Write([]byte(data))  
       this.Ctx.ResponseWriter.Flush()  
       time.Sleep(1 * time.Second)  
    }  
}

sse在请求开始的时候会session read,然后在请求结束的时候session write,登陆前sse请求开始时已经获取了一份session read 存了起来,登陆后存储了一份新的session,而sse断开连接是发生在登陆后,sse在请求结束的时候调用了session write,写了一份空的session数据进去,覆盖了登陆存储的session,从而导致了session不一致。如图:


Pasted image 20231130115249.png

知道问题就简单多了,sse由于不需要保存会话,完全可以不在结束的时候写入session。比如在请求开始seesion read后就直接将 session 设置为nil

context.Input.CruSession = nil

那为什么不在session write的时候加锁,或者写之前再读一次呢?
个人认为锁会影响性能,如果改为乐观锁,则作为框架怎么知道哪个版本的session是旧的,哪个是新的呢,除了开发者没人知道,所以beego在session初始化的时候是没有的,只在session存储kv对的时候做了读写锁的,为什么不写之前再读也是同样的道理。

相关文章

网友评论

      本文标题:记一次长连接导致的session不一致

      本文链接:https://www.haomeiwen.com/subject/ctdfgdtx.html