1. 概述

本文将演示如何为一个基于 Spring Security OAuth 旧版栈 的 OAuth 应用添加登出(Logout)功能。

我们所基于的项目,是之前文章中提到的 使用 OAuth2 构建 REST API。该应用使用了 Spring OAuth 的旧版实现。

注意:本文使用的是 Spring OAuth 旧版项目。如果你正在使用 Spring Security 5 的新栈,应参考我们另一篇文章:在 OAuth 应用中实现登出(新栈)


2. 使访问令牌失效

在 OAuth 保护的应用中,登出的本质是 让当前用户的 Access Token 失效,使其无法再用于访问受保护资源。

如果使用的是 JdbcTokenStore,那这个过程就是从数据库中删除该 token 记录。

我们可以通过为 /oauth/token 接口增加一个 DELETE 方法来实现这一功能。但由于该路径已被 Spring OAuth 框架占用(用于 POST 获取 token 和 GET 查询),我们不能直接在普通 @Controller 中添加该映射,否则会引发冲突或匹配错误。

✅ 正确做法是使用 @FrameworkEndpoint 注解,让这个 endpoint 被 FrameworkEndpointHandlerMapping 处理,而不是标准的 RequestMappingHandlerMapping,从而避免路由冲突。

@FrameworkEndpoint
public class RevokeTokenEndpoint {

    @Resource(name = "tokenServices")
    ConsumerTokenServices tokenServices;

    @RequestMapping(method = RequestMethod.DELETE, value = "/oauth/token")
    @ResponseBody
    public void revokeToken(HttpServletRequest request) {
        String authorization = request.getHeader("Authorization");
        if (authorization != null && authorization.contains("Bearer")){
            String tokenId = authorization.substring("Bearer".length() + 1);
            tokenServices.revokeToken(tokenId);
        }
    }
}

⚠️ 注意:

  • 我们从 Authorization 头中提取 Bearer Token。
  • tokenServices.revokeToken(tokenId) 会同时清除 Access Token 和关联的 Refresh Token(取决于具体实现)。

在之前的文章 处理 Refresh Token 中,我们通过 Zuul 网关的过滤器,将授权服务器返回的 refresh_token 保存到一个 httpOnly 的 cookie 中(名为 refreshToken),以增强安全性。

虽然调用 revokeToken 会使得 Refresh Token 在服务端失效,但 客户端的 cookie 仍然存在。由于 httpOnly cookie 无法通过 JavaScript 删除,我们必须在服务端响应时主动清除它。

解决方案:增强 CustomPostZuulFilter,在拦截到 /oauth/token 的 DELETE 请求时,清除 refreshToken cookie。

@Component
public class CustomPostZuulFilter extends ZuulFilter {
    // ...

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        String requestURI = ctx.getRequest().getRequestURI();
        String requestMethod = ctx.getRequest().getMethod();

        if (requestURI.contains("oauth/token") && requestMethod.equals("DELETE")) {
            Cookie cookie = new Cookie("refreshToken", "");
            cookie.setMaxAge(0); // 立即过期
            cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
            ctx.getResponse().addCookie(cookie);
        }
        // ...
    }
}

✅ 关键点:

  • 设置 MaxAge = 0 表示立即删除 cookie。
  • 路径(path)必须与原始设置一致,否则无法正确清除。

4. 前端 AngularJS 清除 Access Token

除了服务端使 token 失效,前端也需要清除本地的 access_token cookie,并触发登出请求。

我们在 AngularJS 控制器中添加 logout 方法:

$scope.logout = function() {
    logout($scope.loginData);
};

function logout(params) {
    var req = {
        method: 'DELETE',
        url: "oauth/token"
    };
    
    $http(req).then(
        function(data) {
            $cookies.remove("access_token");
            window.location.href = "login";
        },
        function() {
            console.log("登出请求失败");
        }
    );
}

前端登出按钮绑定该方法:

<a class="btn btn-info" href="#" ng-click="logout()">登出</a>

✅ 流程说明:

  1. 发起 DELETE /oauth/token 请求,服务端清除 token。
  2. 成功后,前端删除 access_token cookie。
  3. 跳转至登录页。

⚠️ 踩坑提醒:如果 $cookies.remove 不生效,检查 cookie 的 path 和 domain 是否匹配。AngularJS 默认只操作当前 path 的 cookie。


5. 总结

本文简单粗暴地实现了 OAuth 应用的登出功能,核心步骤如下:

  • ✅ 使用 @FrameworkEndpoint 添加 DELETE /oauth/token 接口,使 Access Token 失效
  • ✅ 在 Zuul 过滤器中清除 httpOnlyrefreshToken cookie
  • ✅ 前端 AngularJS 发起登出请求并清除 access_token cookie

虽然 Spring Security OAuth 旧版已进入维护模式,但在遗留系统中仍广泛使用。理解其 token 管理机制,对维护和升级都至关重要。

完整示例代码见 GitHub:https://github.com/Baeldung/spring-security-oauth/tree/master/oauth-legacy


原始标题:Logout in an OAuth Secured Application (using the Spring Security OAuth legacy stack)