转载

Spring REST API 与 OAuth2:处理 AngularJS 中的 Token 刷新问题

在本教程中,我们将继续探索 之前文章 中提到的 OAuth 密码流,我们将重点介绍如何在 AngularJS 应用中处理 Refresh Token。

2、Access Token 到期

首先,请记住,当用户登录应用程序后,客户端需要得到 Access Token:

function obtainAccessToken(params){
    var req = {
        method: 'POST',
        url: "oauth/token",
        headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
        data: $httpParamSerializer(params)
    }
    $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");
            window.location.href = "login";
        });   
}

请注意我们的 Access Token 存储在 Cookie 中,该 cookie 将根据令牌本身到期的时间过期。

重要的一点: cookie 本身只用于存储 ,它不会在 OAuth 流中驱动任何其他东西。例如,浏览器永远不会主动通过请求将 cookie 发送到服务器。

还要注意应该如何调用这个 getsAccessToken() 函数:

$scope.loginData = {
    grant_type:"password", 
    username: "", 
    password: "", 
    client_id: "fooClientIdPassword"
};
 
$scope.login = function(){   
    obtainAccessToken($scope.loginData);
}

3、代理

我们现在要在前端应用程序中运行一个 Zuul 代理,位于前端客户端和授权服务器之间。

让我们配置代理路由:

zuul:
routes:
oauth:
path: /oauth/**
url: http://localhost:8081/spring-security-oauth-server/oauth

有趣的是,我们只是代理授权服务器的流量,而没有做其他事情。当客户端获取新的令牌时,我们才真正需要代理。

如果您想了解 Zuul 的基础知识,可参阅 《Spring REST 与 Zuul 代理》(可在发文历史中找到)。

4、执行 Basic Authentication 的 Zuul Filter

使用代理很简单,您不用在 javascript 中声明应用程序的 客户端密钥 ,我们将使用 Zuul 前置过滤器来将授权头添加到获取访问令牌的请求中:

@Component
public class CustomPreZuulFilterextends ZuulFilter{
    @Override
    public Object run(){
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.getRequest().getRequestURI().contains("oauth/token")) {
            byte[] encoded;
            try {
                encoded = Base64.encode("fooClientIdPassword:secret".getBytes("UTF-8"));
                ctx.addZuulRequestHeader("Authorization", "Basic " + new String(encoded));
            } catch (UnsupportedEncodingException e) {
                logger.error("Error occured in pre filter", e);
            }
        }
        return null;
    }
 
    @Override
    public boolean shouldFilter(){
        return true;
    }
 
    @Override
    public int filterOrder(){
        return -2;
    }
 
    @Override
    public String filterType(){
        return "pre";
    }
}

请注意,这样不会增加任何额外的安全保障,我们这样做的目的是因为使用了客户端凭据的 Basic Authentication 来保护令牌端点。

从实现的角度来看,需要特别注意此过滤器的类型。我们使用“前置”类型的过滤器来处理请求,之后再把请求传递下去。

5、将 Refresh Token 放在 Cookie 中

我们计划在这里让客户端将刷新令牌作为一个 cookie,但这不是一个普通的 cookie,而是有一个有着安全的限制路径(/oauth/token)和 HTTP-only 的 cookie。

我们将设置一个 Zuul 后置过滤器,从响应的 JSON 正文中提取 Refresh Token,并将其设置到 cookie 中:

@Component
public class CustomPostZuulFilterextends ZuulFilter{
    private ObjectMapper mapper = new ObjectMapper();
 
    @Override
    public Object run(){
        RequestContext ctx = RequestContext.getCurrentContext();
        try {
            InputStream is = ctx.getResponseDataStream();
            String responseBody = IOUtils.toString(is, "UTF-8");
            if (responseBody.contains("refresh_token")) {
                Map<String, Object> responseMap = mapper.readValue(
                  responseBody, new TypeReference<Map<String, Object>>() {});
                String refreshToken = responseMap.get("refresh_token").toString();
                responseMap.remove("refresh_token");
                responseBody = mapper.writeValueAsString(responseMap);
 
                Cookie cookie = new Cookie("refreshToken", refreshToken);
                cookie.setHttpOnly(true);
                cookie.setSecure(true);
                cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
                cookie.setMaxAge(2592000); // 30 days
                ctx.getResponse().addCookie(cookie);
            }
            ctx.setResponseBody(responseBody);
        } catch (IOException e) {
            logger.error("Error occured in zuul post filter", e);
        }
        return null;
    }
 
    @Override
    public boolean shouldFilter(){
        return true;
    }
 
    @Override
    public int filterOrder(){
        return 10;
    }
 
    @Override
    public String filterType(){
        return "post";
    }
}

您需要了解几件事:

  • 我们使用 Zuul 后置过滤器来读取响应并 提取刷新令牌
  • 我们从 JSON 响应中删除了 refresh_token 的值,以确保它不能在 cookie 之外的前端被访问
  • 我们将 cookie 的 max age 设置为 30 天,这符合令牌的过期时间

6、从 Cookie 获取并使用 Refresh Token

我们在 cookie 中有了 Refresh Token,当前端 AngularJS 应用尝试触发令牌刷新时,它会将请求发送到 /oauth/token ,因此浏览器当然会发送该 cookie。

因此,我们现在将在代理中使用另一个过滤器,从 Cookie 中提取 Refresh Token,并将其作为 HTTP 参数发送,是的该请求是有效的:

public Object run(){
    RequestContext ctx = RequestContext.getCurrentContext();
    ...
    HttpServletRequest req = ctx.getRequest();
    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));
    }
    ...
}
 
private String extractRefreshToken(HttpServletRequest req){
    Cookie[] cookies = req.getCookies();
    if (cookies != null) {
        for (int i = 0; i < cookies.length; i++) {
            if (cookies[i].getName().equalsIgnoreCase("refreshToken")) {
                return cookies[i].getValue();
            }
        }
    }
    return null;
}

以下是我们的 CustomHttpServletRequest — 用于注入我们的刷新令牌参数:

public class CustomHttpServletRequestextends HttpServletRequestWrapper{
    private Map<String, String[]> additionalParams;
    private HttpServletRequest request;
 
    public CustomHttpServletRequest(
      HttpServletRequest request, Map<String, String[]> additionalParams) {
        super(request);
        this.request = request;
        this.additionalParams = additionalParams;
    }
 
    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> map = request.getParameterMap();
        Map<String, String[]> param = new HashMap<String, String[]>();
        param.putAll(map);
        param.putAll(additionalParams);
        return param;
    }
}

同样,这里有许多重要的实现要点:

  • 代理从 Cookie 中提取 Refresh Token
  • 之后将其设置到 refresh_token 参数
  • 它也将 grant_type 设置到 refresh_token
  • 如果没有 refreshToken cookie(过期或第一次登录),则 Access Token 请求将被重定向,而不会作出任何改变

7、AngularJS 刷新 Access Token

最后,让我们修改前端应用,并使用令牌刷新:

以下是我们的函数 refreshAccessToken()

$scope.refreshAccessToken = function(){
    obtainAccessToken($scope.refreshData);
}

以及 $scope.refreshData

$scope.refreshData = {grant_type:"refresh_token"};

请注意,我们简单地使用了现有的 getAccessToken 函数 — 只是传入的参数不同。

还要注意的是,我们没有添加 refresh_token ,因为这属于 Zuul 过滤器负责。

8、结论

在此 OAuth 教程中,我们学习了如何在 AngularJS 客户端应用中存储 Refresh Token、如何刷新过期的 Access Token 以及如何利用 Zuul 代理这些工作。

本教程的完整实现可以在 项目 github 中找到 — 这是一个基于 Eclipse 的项目,因此应该很容易导入和运行。

原文  http://oopsguy.com/2017/11/07/spring-security-oauth2-refresh-token-angular-js/
正文到此结束
Loading...