OAuth2 Remember Me with Refresh Token (using the Spring Security OAuth legacy stack) – 使用刷新令牌的OAuth2记住我(使用Spring Security OAuth传统栈)

最后修改: 2017年 7月 3日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we will add a “Remember Me” functionality to an OAuth 2 secured application, by leveraging the OAuth 2 Refresh Token.

在这篇文章中,我们将通过利用OAuth 2刷新令牌,为OAuth 2安全应用程序添加 “记住我 “的功能。

This article is a continuation of our series on using OAuth 2 to secure a Spring REST API, which is accessed through an AngularJS Client. For setting up the Authorization Server, Resource Server, and front-end Client, you can follow the introductory article.

本文是我们关于使用 OAuth 2 来保护 Spring REST API 的系列文章的延续,该 API 可通过 AngularJS 客户端进行访问。对于设置授权服务器、资源服务器和前端客户端,您可以遵循介绍性文章

Note: this article is using the Spring OAuth legacy project.

注意:本文使用的是Spring OAuth遗留项目

2. OAuth 2 Access Token and Refresh Token

2.OAuth 2访问令牌和刷新令牌

First, let’s do a quick recap on the OAuth 2 tokens and how they can be used.

首先,让我们快速回顾一下OAuth 2令牌以及如何使用它们。

On a first authentication attempt using the password grant type, the user needs to send a valid username and password, as well as the client id and secret. If the authentication request is successful, the server sends back a response of the form:

在使用password授予类型的第一次认证尝试中,用户需要发送一个有效的用户名和密码,以及客户端ID和秘密。如果认证请求成功,服务器会发回一个格式的响应。

{
    "access_token": "2e17505e-1c34-4ea6-a901-40e49ba786fa",
    "token_type": "bearer",
    "refresh_token": "e5f19364-862d-4212-ad14-9d6275ab1a62",
    "expires_in": 59,
    "scope": "read write",
}

We can see the server response contains both an access token, as well as a refresh token. The access token will be used for subsequent API calls that require authentication, while the purpose of the refresh token is to obtain a new valid access token or just revoke the previous one.

我们可以看到服务器响应包含一个访问令牌以及一个刷新令牌。访问令牌将用于后续需要认证的API调用,而刷新令牌的目的是获得一个新的有效的访问令牌或只是撤销之前的访问令牌。

To receive a new access token using the refresh_token grant type, the user no longer needs to enter their credentials, but only the client id, secret and of course the refresh token.

要使用refresh_token授予类型接收新的访问令牌,用户不再需要输入他们的凭证,而只需要输入客户端ID、秘密,当然还有refresh令牌。

The goal of using two types of tokens is to enhance user security. Typically the access token has a shorter validity period so that if an attacker obtains the access token, they have a limited time in which to use it. On the other hand, if the refresh token is compromised, this is useless as the client id and secret are also needed.

使用两类令牌的目的是为了增强用户的安全性。通常情况下,访问令牌的有效期较短,因此如果攻击者获得访问令牌,他们有有限的时间来使用它。另一方面,如果刷新令牌被破坏,这就没有用了,因为还需要客户端ID和秘密。

Another benefit of refresh tokens is that it allows revoking the access token, and not sending another one back if the user displays unusual behavior such as logging in from a new IP.

刷新令牌的另一个好处是,它允许撤销访问令牌,如果用户表现出不寻常的行为,如从一个新的IP登录,就不会再发回另一个令牌。

3. Remember-Me Functionality With Refresh Tokens

3.用刷新令牌记住我的功能

Users usually find it useful to have the option to preserve their session, as they don’t need to enter their credentials every time they access the application.

用户通常认为有保留会话的选项很有用,因为他们不需要在每次访问应用程序时都输入他们的凭证。

Since the Access Token has a shorter validity time, we can instead make use of refresh tokens to generate new access tokens and avoid having to ask the user for their credentials every time an access token expires.

由于访问令牌的有效期较短,我们反而可以利用刷新令牌来生成新的访问令牌,避免每次访问令牌过期时都要向用户索要凭证。

In the next sections, we’ll discuss two ways of implementing this functionality:

在接下来的章节中,我们将讨论实现这一功能的两种方法。

  • first, by intercepting any user request that returns a 401 status code, which means the access token is invalid. When this occurs, if the user has checked the “remember me” option, we’ll automatically issue a request for a new access token using refresh_token grant type, then execute the initial request again.
  • second, we can refresh the Access Token proactively – we’ll send a request to refresh the token a few seconds before it expires

The second option has the advantage that the user’s requests will not be delayed.

第二种方案的好处是用户的请求不会被延迟。

4. Storing the Refresh Token

4.存储刷新令牌

In the previous article on Refresh Tokens, we added a CustomPostZuulFilter which intercepts requests to the OAuth server, extracts the refresh token sent back on authentication, and stores it in a server-side cookie:

上一篇关于刷新令牌的文章中,我们添加了一个CustomPostZuulFilter,它拦截对OAuth服务器的请求,提取认证时发回的刷新令牌,并将其存储在服务器端的 cookie 中。

@Component
public class CustomPostZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        //...
        Cookie cookie = new Cookie("refreshToken", refreshToken);
        cookie.setHttpOnly(true);
        cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
        cookie.setMaxAge(2592000); // 30 days
        ctx.getResponse().addCookie(cookie);
        //...
    }
}

Next, let’s add a checkbox on our login form that has a data binding to the loginData.remember variable:

接下来,让我们在登录表单上添加一个复选框,它与loginData.remember变量有数据绑定。

<input type="checkbox"  ng-model="loginData.remember" id="remember"/>
<label for="remember">Remeber me</label>

Our login form will now display an additional checkbox:

我们的登录表格现在将显示一个额外的复选框。

remember

The loginData object is sent with the authentication request, so it will include the remember parameter. Before the authentication request is sent, we will set a cookie named remember based on the parameter:

loginData对象与认证请求一起发送,所以它将包括remember参数。在发送认证请求之前,我们将根据该参数设置一个名为remember的cookie。

function obtainAccessToken(params){
    if (params.username != null){
        if (params.remember != null){
            $cookies.put("remember","yes");
        }
        else {
            $cookies.remove("remember");
        }
    }
    //...
}

As a consequence, we’ll check this cookie to determine whether we should attempt to refresh the access token or not, depending on whether the user wishes to be remembered or not.

因此,我们将检查这个cookie,以确定我们是否应该尝试刷新访问令牌,这取决于用户是否希望被记住。

5. Refreshing Tokens by Intercepting 401 Responses

5.通过拦截401响应来刷新令牌

To intercept requests that come back with a 401 response, let’s modify our AngularJS application to add an interceptor with a responseError function:

为了拦截返回401响应的请求,让我们修改我们的AngularJS应用程序,添加一个带有responseError函数的拦截器。

app.factory('rememberMeInterceptor', ['$q', '$injector', '$httpParamSerializer', 
  function($q, $injector, $httpParamSerializer) {  
    var interceptor = {
        responseError: function(response) {
            if (response.status == 401){
                
                // refresh access token

                // make the backend call again and chain the request
                return deferred.promise.then(function() {
                    return $http(response.config);
                });
            }
            return $q.reject(response);
        }
    };
    return interceptor;
}]);

Our function checks if the status is 401 – which means the Access Token is invalid, and if so, attempts to use the Refresh Token in order to obtain a new valid Access Token.

我们的函数检查状态是否为401–这意味着访问令牌无效,如果是,则尝试使用刷新令牌以获得新的有效访问令牌。

If this is successful, the function continues to re-try the initial request which resulted in the 401 error. This ensures a seamless experience for the user.

如果这是成功的,该功能将继续重试导致401错误的初始请求。这确保了用户的无缝体验。

Let’s take a closer look at the process of refreshing the access token. First, we will initialize the necessary variables:

让我们仔细看看刷新访问令牌的过程。首先,我们将初始化必要的变量。

var $http = $injector.get('$http');
var $cookies = $injector.get('$cookies');
var deferred = $q.defer();

var refreshData = {grant_type:"refresh_token"};
                
var req = {
    method: 'POST',
    url: "oauth/token",
    headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
    data: $httpParamSerializer(refreshData)
}

You can see the req variable which we will use to send a POST request to the /oauth/token endpoint, with parameter grant_type=refresh_token.

你可以看到req变量,我们将用它向/oauth/token端点发送一个POST请求,参数为grant_type=refresh_token

Next, let’s use the $http module we have injected to send the request. If the request is successful, we will set a new Authentication header with the new access token value, as well as a new value for the access_token cookie. If the request fails, which may happen if the refresh token also eventually expires, then the user is redirected to the login page:

接下来,让我们使用我们已经注入的$http模块来发送请求。如果请求成功,我们将用新的访问令牌值设置一个新的Authentication头,并为access_token cookie设置一个新值。如果请求失败,也可能发生在刷新令牌最终过期的情况下,那么用户将被重定向到登录页面。

$http(req).then(
    function(data){
        $http.defaults.headers.common.Authorization= 'Bearer ' + data.data.access_token;
        var expireDate = new Date (new Date().getTime() + (1000 * data.data.expires_in));
        $cookies.put("access_token", data.data.access_token, {'expires': expireDate});
        window.location.href="index";
    },function(){
        console.log("error");
        $cookies.remove("access_token");
        window.location.href = "login";
    }
);

The Refresh Token is added to the request by the CustomPreZuulFilter we implemented in the previous article:

刷新令牌是由我们在上一篇文章中实现的CustomPreZuulFilter添加到请求中。

@Component
public class CustomPreZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        //...
        String refreshToken = extractRefreshToken(req);
        if (refreshToken != null) {
            Map<String, String[]> param = new HashMap<String, String[]>();
            param.put("refresh_token", new String[] { refreshToken });
            param.put("grant_type", new String[] { "refresh_token" });

            ctx.setRequest(new CustomHttpServletRequest(req, param));
        }
        //...
    }
}

In addition to defining the interceptor, we need to register it with the $httpProvider:

除了定义拦截器,我们还需要将其注册到$httpProvider

app.config(['$httpProvider', function($httpProvider) {  
    $httpProvider.interceptors.push('rememberMeInterceptor');
}]);

6. Refreshing Tokens Proactively

6.主动刷新令牌

Another way to implement the “remember-me” functionality is by requesting a new access token before the current one expires.

实现 “记住我 “功能的另一种方式是在当前访问令牌到期之前请求一个新的访问令牌。

When receiving an access token, the JSON response contains an expires_in value that specifies the number of seconds that the token will be valid for.

当收到一个访问令牌时,JSON响应包含一个expires_in值,指定令牌的有效秒数。

Let’s save this value in a cookie for each authentication:

让我们把这个值保存在每次认证的cookie中。

$cookies.put("validity", data.data.expires_in);

Then, to send a refresh request, let’s use the AngularJS $timeout service to schedule a refresh call 10 seconds before the token expires:

然后,为了发送刷新请求,让我们使用AngularJS $timeout服务,在令牌过期前10秒安排一次刷新调用。

if ($cookies.get("remember") == "yes"){
    var validity = $cookies.get("validity");
    if (validity >10) validity -= 10;
    $timeout( function(){ $scope.refreshAccessToken(); }, validity * 1000);
}

7. Conclusion

7.结论

In this tutorial, we’ve explored two ways we can implement “Remember Me” functionality with an OAuth2 application and an AngularJS front-end.

在本教程中,我们探讨了用OAuth2应用程序和AngularJS前端实现 “记住我 “功能的两种方法。

The full source code of the examples can be found over on GitHub. You can access the login page with “remember me” functionality at the URL /login_remember.

示例的完整源代码可以在GitHub上找到over。你可以通过URL /login_remember访问具有 “记住我 “功能的登录页面。