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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 14:34:56