[譯] Spring REST API 與 OAuth2:處理 AngularJS 中的 Token 重新整理問題

Oopsguy發表於2019-02-26

www.baeldung.com/spring-secu…
作者:Eugen Paraschiv
譯者:oopsguy
公眾號:oopsguy_com

1、概述

在本教程中,我們將繼續探索之前文章中提到的 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 CustomPreZuulFilter extends 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 CustomPostZuulFilter extends 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 CustomHttpServletRequest extends 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 的專案,因此應該很容易匯入和執行。

原文例項程式碼

github.com/eugenp/spri…

相關文章