1. 概述
在本教程中,我們將繼續探索我們在上一篇文章中開始構建的OAuth密碼流,並將重點放在如何處理AngularJS應用程序中的刷新令牌上。
注意:本文使用 Spring OAuth 遺留項目。 對於使用新 Spring Security 5 堆棧的本文版本,請參閲我們的文章 OAuth2 for a Spring REST API – Handle the Refresh Token in Angular。
2. 訪問令牌到期
首先,請記住,客户端是在用户登錄應用程序時獲取訪問令牌的:
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";
});
}
請注意,我們的訪問令牌存儲在 cookie 中,該 cookie 將根據令牌本身的到期時間而過期。
重要的是理解的是,該 cookie 僅用於存儲,它不會驅動 OAuth 流中的任何其他內容。例如,瀏覽器永遠不會自動將 cookie 發送到服務器與請求一起。
請注意我們實際調用此obtainAccessToken()函數的方式:
$scope.loginData = {
grant_type:"password",
username: "",
password: "",
client_id: "fooClientIdPassword"
};
$scope.login = function() {
obtainAccessToken($scope.loginData);
}
3. 代理 (Proxy)
我們現在將在前端應用程序中運行一個 Zuul 代理,它基本上位於前端客户端和授權服務器之間。
讓我們配置代理的路由:
zuul:
routes:
oauth:
path: /oauth/**
url: http://localhost:8081/spring-security-oauth-server/oauth
這裏有趣的是,我們只代理流量到授權服務器,而沒有其他任何內容。我們只需要代理來接收客户端獲取新令牌時。
如果您想了解 Zuul 的基本知識,請閲讀主要的 Zuul 文章。
4. 一個進行基本身份驗證的 Zuul 過濾器
Zuul 代理的首要用途非常簡單——而不是在 JavaScript 中暴露我們的應用程序 “client secret”,我們將使用一個 Zuul 預過濾器,為訪問令牌請求添加 Authorization 標頭:@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("預過濾器中發生錯誤", e);
}
}
return null;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public int filterOrder() {
return -2;
}
@Override
public String filterType() {
return "pre";
}
}
請注意,這不會添加任何額外的安全措施,我們這樣做只是因為令牌端點使用客户端憑據進行基本身份驗證。
從實現角度來看,過濾器的類型尤其值得注意。我們使用“pre”類型的過濾器來在將請求傳遞給下一個處理程序之前對其進行處理。
5. 將 Refresh Token 放入 Cookie
進入有趣的部分。我們計劃在這裏讓客户端以 Cookie 的形式獲取 Refresh Token。不僅僅是一個普通的 Cookie,而是一個受保護的、僅限 HTTP 的 Cookie,並且路徑非常有限 (/oauth/token)。
我們將設置一個 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 responseMap = mapper.readValue(
responseBody, new TypeReference
這裏有一些有趣的事情需要理解:
- 我們使用了 Zuul 後置過濾器來讀取響應並提取 Refresh Token
- 我們從 JSON 響應中刪除了 refresh_token 的值,以確保它永遠無法被前端在 Cookie 之外訪問
- 我們設置了 Cookie 的 max-age 為 30 天 – 匹配了令牌的過期時間
為此,我們將創建一個配置類:
@Configuration
public class SameSiteConfig implements WebMvcConfigurer {
@Bean
public TomcatContextCustomizer sameSiteCookiesConfig() {
return context -> {
final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
cookieProcessor.setSameSiteCookies(SameSiteCookies.STRICT.getValue());
context.setCookieProcessor(cookieProcessor);
};
}
}
在這裏,我們將屬性設置為 strict,以便任何跨站點 Cookie 傳輸都將被嚴格拒絕。
6. 從 Cookie 中獲取並使用 Refresh Token
現在我們已經有了 Cookie 中的 Refresh Token,當前端 AngularJS 應用程序嘗試觸發 Token 刷新時,它將會向 /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 ——用於 注入我們的 Refresh Token 參數
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 刷新訪問令牌
最後,讓我們修改我們的簡單前端應用程序並真正利用刷新令牌的功能:
這是我們的函數 refreshAccessToken():
$scope.refreshAccessToken = function() {
obtainAccessToken($scope.refreshData);
}
以及我們的 $scope.refreshData:
$scope.refreshData = {grant_type:"refresh_token"};
請注意,我們只是使用現有的 obtainAccessToken 函數,並將其傳遞不同的輸入。
另外,請注意我們沒有自己添加 refresh_token,因為這將會由 Zuul 過濾器來處理。
8. 結論
在本 OAuth 教程中,我們學習瞭如何在 AngularJS 客户端應用程序中存儲 Refresh Token,如何刷新過期的 Access Token,以及如何利用 Zuul 代理來實現這一切。