如何在Backbone中处理服务器的登出确认请求
项目采用Java8 Servlet/Restlet后端+Backbone前端,原有登出按钮可正常终止用户会话并跳转至登录界面。升级Java框架至最新版本后,后端新增登出确认要求,但Backbone代码未做适配,导致点击登出按钮后仅刷新当前页,无法跳转登录界面,只能通过关闭浏览器并重启后端完成登出。
原框架网络请求逻辑:GET /logout返回200状态码,随后跳转至登录页
新框架网络请求逻辑:GET /logout返回401状态码,触发GET /logoutConfirm请求,最终页面刷新但未完成登出跳转
以下为剔除业务代码后的授权与登出相关简化代码:
function AppAuth() { return { _currentUser: undefined, _redirectTo: undefined, isAuthorized: function () { return app.cookieCutter.get('apphash') !== undefined; }, currentUser: function () { return this._currentUser; }, // If user authorised, just return. If not, change the hash to /login authorizedOrRedirect: function () { if (!app.auth.isAuthorized() && window.location.hash != '/login') { app.auth._redirectTo = window.location.hash; // Save current page to go back to this.logout(); return false; } }, logout: function () { if (this._currentUser != undefined) { this._currentUser.trigger('logout'); } this._currentUser = undefined; $.ajax("/logout", { type: "GET" }); var cookies = app.cookies.keys(); _.forEach(cookies, function (cookie) { app.cookieCutter.remove(cookie); }); window.location.pathname = "/logout"; return true; }, }; } app = {}; app.traceUrlEvents = true; // Comment out if NOT CORS requests. In our case they are until release: app.apiIsCrossOrigin = true; jQuery.support.cors = true; // Remote working app.apiRoot = '/v1'; app.loginEndpoint = '/login'; // Localhost app.auth = APPAuth(); if (app._queueSameGetRequests) { Backbone.xhrs = {}; var oldFetch = Backbone.Model.prototype.fetch; Backbone.Model.prototype.fetch = function (opts) { var self = this; var args = arguments; // cache GET requests, not the others if (!opts || opts['cache'] !== true) { return oldFetch.apply(this, arguments); } var promise; var r; if (_.isFunction(this.url)) { r = this.url(); } else { r = this.url; } // issue the request if a cached version does not exist if (!Backbone.xhrs[r]) { promise = Backbone.xhrs[r] = oldFetch.apply(this, arguments); } else { var p = Backbone.xhrs[r].done(function () { oldFetch.apply(self, args); }); return p; } return promise; }; } // Default error handlers for network and other XHR errors app.networkAuthErrorHandler = function () { app.auth.logout(); // Refresh the window, this should cause redirection to login screen. Could loop ? console.log("302 from API, authorization required"); app.auth.authorizedOrRedirect(); }; app.networkErrorHandler = function () { $('#global-warn').show(); }; // Handle transport errors/network being down $(document).ajaxError(function (evt, resp, request, txt) { if (resp.status === 0) { app.networkErrorHandler(); } }); app.Router = {}; app.boot = function () { app.EventObject = {}; _.extend(app.EventObject, Backbone.Events); new app.controllers.DefaultController(); app.Router = new app.controllers.Auth(); new app.controllers.Users(); // Force authorisation check when using navigate from within app var triggerNewRoute = function () { console.log("Route change", arguments); app.EventObject.trigger('routeChange'); app.auth.authorizedOrRedirect(); }; Backbone.$(window).on('hashchange', triggerNewRoute); if (app.traceUrlEvents) { Backbone.$(window).on('pushstate', urlTraces); Backbone.$(window).on('popstate', urlTraces); Backbone.$(window).on('hashchange', urlTraces); } }; app.models.BaseModel = Backbone.Model.extend({ apiRoot: function () { return app.apiRoot; }, // Overwrite to support trailing slashes url: function () { var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || this.urlError(); if (this.isNew()) return base; return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id) + '/'; }, }); (function (global) { 'use strict'; })(this); app.controllers.Auth = app.controllers.BaseController.extend({ routes: { "auth/": "getAuthPage" }, getAuthPage: function () { var loginView = new app.views.LoginView(); app.rootView.renderMainView(loginView, 'login'); if (app.auth.isAuthorized()) { app.Router.navigate('//', { trigger: true }); } } }); // Router/controller for the user list page app.controllers.Users = app.controllers.BaseController.extend({ "routes": { "logout/": "logoutCurrentUser" }, logoutCurrentUser: function () { console.log(">>>>>>> logoutCurrentUser() is empty function !!") } }); // Render the title bar app.views.Headerbar = Backbone.View.extend({ template: 'topbar', className: 'navbar navbar-main', events: { 'click .logoutlink': 'doLogout', }, doLogout: function (e) { e.preventDefault(); app.auth.logout(); window.location = app.loginEndpoint; return false; } });
1. 改造AppAuth.logout方法,适配后端登出确认逻辑
原代码中$.ajax("/logout")为异步请求,且未处理响应,导致清除Cookie和跳转逻辑在登出请求完成前执行,新框架返回401要求确认时,登出操作并未实际完成。
修改后的代码:
logout: function () { if (this._currentUser != undefined) { this._currentUser.trigger('logout'); } this._currentUser = undefined; // 等待登出请求响应,处理确认逻辑 $.ajax("/logout", { type: "GET", success: function() { clearCookiesAndRedirect(); }, error: function(xhr) { // 后端返回401时,发起登出确认请求 if (xhr.status === 401) { $.ajax("/logoutConfirm", { type: "GET", success: clearCookiesAndRedirect, error: clearCookiesAndRedirect // 确认失败也强制清除凭证跳转 }); } else { clearCookiesAndRedirect(); } } }); // 提取清除凭证和跳转的公共逻辑 function clearCookiesAndRedirect() { var cookies = app.cookies.keys(); _.forEach(cookies, function (cookie) { app.cookieCutter.remove(cookie); }); // 直接跳转登录页,避免再次触发/logout路径 window.location.pathname = app.loginEndpoint; } return true; },
原因:
- 必须等待后端登出请求(及确认请求)完成后,再清除用户凭证并跳转,确保会话已正确终止
- 原代码跳转到
/logout会再次触发登出流程,改为直接跳转到app.loginEndpoint避免循环
2. 移除Headerbar.doLogout中的冗余跳转逻辑
原方法中调用app.auth.logout()后又执行window.location = app.loginEndpoint,会导致重复跳转,干扰异步请求的正常处理:
doLogout: function (e) { e.preventDefault(); app.auth.logout(); return false; }
原因:app.auth.logout()已包含完整的清除凭证和跳转逻辑,额外的跳转操作会在登出请求未完成时触发,导致登出失效。
3. 优化networkAuthErrorHandler逻辑,避免循环跳转
原方法调用app.auth.logout()后又执行authorizedOrRedirect(),可能引发循环跳转,修改为直接清除凭证并跳转:
app.networkAuthErrorHandler = function () { app.auth._currentUser = undefined; var cookies = app.cookies.keys(); _.forEach(cookies, function (cookie) { app.cookieCutter.remove(cookie); }); window.location.pathname = app.loginEndpoint; console.log("302 from API, authorization required"); };
原因:遇到授权错误时,直接清除凭证跳转登录页,避免重复发起登出请求,减少不必要的网络交互。
4. 补全Users.logoutCurrentUser函数逻辑
原函数为空,导致通过路由#logout/触发的登出无实际操作:
logoutCurrentUser: function () { app.auth.logout(); }
原因:确保用户通过直接访问登出路由时,也能执行完整的登出流程。
内容的提问来源于stack exchange,提问作者Gaz

