You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails 5应用登录后偶现AJAX响应替代视图的原因

问题:登录后偶尔显示AJAX请求的JSON响应而非登录视图

我在Rails 5应用中有一个提交登录表单的按钮,该按钮会向Session控制器发起POST请求,请求日志如下:

Started POST "/login" for 127.0.0.1 at 2024-04-03 10:49:13 +0300
Processing by SessionsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"B1kNyKftmEga+Dz7UZ25m4KUDZCn5p7z1qdsC/r84gPhbD9hPNmc4q4WH4IJdos+QS0LDAm0YMX8YpxpzWtPCw==", "session"=>{"email"=>"mail@yandex.ru", "password"=>"[FILTERED]"}, "lang"=>"en"} 

路由配置:

post    '/login',      to: 'sessions#create'

同时我有一个用于更新用户令牌的AJAX方法:

setTimeout( () => {
if (typeof user_grants !== 'undefined') {
  if(!channels['user_session']) {
    userSessionChannel = new Channel("UserSessionChannel");
    userSessionChannel.socketSubscribe();
    userSessionChannel.socketCallBack( {event: 'user_login'} , "user_session", userSessionWebsocketRouter);
    userSessionChannel.socketTrackConnection(toggleDisconnectedModal, 'offline', websocket_keep_alive);
    channels['user_session'] = userSessionChannel;
  }
  const jwtUpdater = new PeriodicRunner(() => {
    $.ajax({
      url: '/renew_jwt',
      type: 'GET',
      success: function (res) {
        if (res.ok) {
          app_config.jwt_encoded_user = res.data;
          ajaxSetup();
        }
      },
      error: function (error) {
        console.log(error);
      },
    });
  }, 300).start()
}
}, 0);

对应的路由配置:

get   '/renew_jwt', to: 'application#renew_jwt'

控制器实现:

def renew_jwt
  return render json: {ok: true, data: jwt_encode_user}
end

def jwt_encode_user
  if Rails.env.stage?
    key = Rails.application.credentials.secret_key_base_dev
  elsif Rails.env.production?
    key = Rails.application.credentials.secret_key_base
  else
    key = Rails.application.credentials.secret_key_base_dev
  end
  payload = { user_id: current_user.id.to_s, exp: ((Time.now.to_f * 1000) + (60000 * 10)).to_i }
  response.headers['Cache-Control'] = 'no-cache'
  return JWT.encode payload, key, 'HS256'
end

但偶尔会出现非常罕见的情况:登录后显示的是该AJAX请求的响应,而非登录提交请求对应的视图。请问这是什么原因?


原因分析及解决方案

核心原因:请求竞态条件(Race Condition)

这是两个请求几乎同时发起导致的极端场景:

  • 用户点击登录按钮时,浏览器触发表单的同步POST登录请求,同时setTimeout(0)的回调会立即进入事件队列,在表单提交的导航逻辑执行前,启动了renew_jwt的AJAX GET请求。
  • 两个请求同属一个会话,若服务器处理renew_jwt的速度远快于登录请求(比如登录需要查询数据库、验证密码,而令牌生成只是简单加密操作),AJAX的JSON响应会先到达浏览器。
  • 在网络波动或浏览器连接复用的特殊情况下,浏览器可能错误地将这个JSON响应关联到正在等待的登录页面导航请求,导致页面渲染出JSON内容而非登录后的视图。

其他辅助因素

  1. setTimeout(0)的执行时机偏差:setTimeout(fn, 0)会把回调放到当前任务队列末尾,但浏览器处理表单同步导航和JS事件队列的顺序可能出现偏差,导致AJAX请求提前发起。
  2. 会话状态提前暴露:如果SessionsController#create在完成所有会话初始化(比如设置session cookie、标记用户登录状态)之前就释放了会话锁,renew_jwt请求可能提前获取到登录后的current_user并返回令牌,进一步加剧竞态冲突。

解决方案

  1. 延迟AJAX任务启动时机:不要在全局setTimeout(0)中启动令牌更新,而是在登录成功后的页面加载完成后再初始化。比如:
    • 在登录成功的视图模板中添加初始化脚本,或监听Turbolinks的turbolinks:load事件(若使用Turbolinks),确保页面完全加载后再启动PeriodicRunner。
  2. 明确AJAX请求的数据类型:在$.ajax配置中添加dataType: 'json',强制浏览器识别响应为JSON,避免错误渲染:
    $.ajax({
      url: '/renew_jwt',
      type: 'GET',
      dataType: 'json',
      // ... 其他配置
    });
    
  3. 优化登录请求处理逻辑:确保SessionsController#create在完成所有登录逻辑(包括会话状态设置、cookie写入)后再返回响应,避免会话状态提前暴露。
  4. 改用POST请求更新令牌:将renew_jwt的GET请求改为POST,并添加CSRF令牌验证,既增强安全性,也能减少GET请求可能带来的缓存或意外触发问题。

内容的提问来源于stack exchange,提问作者Anderson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 20:17:04