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内容而非登录后的视图。
其他辅助因素
setTimeout(0)的执行时机偏差:setTimeout(fn, 0)会把回调放到当前任务队列末尾,但浏览器处理表单同步导航和JS事件队列的顺序可能出现偏差,导致AJAX请求提前发起。- 会话状态提前暴露:如果SessionsController#create在完成所有会话初始化(比如设置session cookie、标记用户登录状态)之前就释放了会话锁,
renew_jwt请求可能提前获取到登录后的current_user并返回令牌,进一步加剧竞态冲突。
解决方案
- 延迟AJAX任务启动时机:不要在全局
setTimeout(0)中启动令牌更新,而是在登录成功后的页面加载完成后再初始化。比如:- 在登录成功的视图模板中添加初始化脚本,或监听Turbolinks的
turbolinks:load事件(若使用Turbolinks),确保页面完全加载后再启动PeriodicRunner。
- 在登录成功的视图模板中添加初始化脚本,或监听Turbolinks的
- 明确AJAX请求的数据类型:在
$.ajax配置中添加dataType: 'json',强制浏览器识别响应为JSON,避免错误渲染:$.ajax({ url: '/renew_jwt', type: 'GET', dataType: 'json', // ... 其他配置 }); - 优化登录请求处理逻辑:确保SessionsController#create在完成所有登录逻辑(包括会话状态设置、cookie写入)后再返回响应,避免会话状态提前暴露。
- 改用POST请求更新令牌:将
renew_jwt的GET请求改为POST,并添加CSRF令牌验证,既增强安全性,也能减少GET请求可能带来的缓存或意外触发问题。
内容的提问来源于stack exchange,提问作者Anderson
相关产品推荐
相关产品推荐

