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 的專案,因此應該很容易匯入和執行。