在本教程中,我们将继续探索 之前文章 中提到的 OAuth 密码流,我们将重点介绍如何在 AngularJS 应用中处理 Refresh 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); }
我们现在要在前端应用程序中运行一个 Zuul 代理,位于前端客户端和授权服务器之间。
让我们配置代理路由:
zuul: routes: oauth: path: /oauth/** url: http://localhost:8081/spring-security-oauth-server/oauth
有趣的是,我们只是代理授权服务器的流量,而没有做其他事情。当客户端获取新的令牌时,我们才真正需要代理。
如果您想了解 Zuul 的基础知识,可参阅 《Spring REST 与 Zuul 代理》(可在发文历史中找到)。
使用代理很简单,您不用在 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 来保护令牌端点。
从实现的角度来看,需要特别注意此过滤器的类型。我们使用“前置”类型的过滤器来处理请求,之后再把请求传递下去。
我们计划在这里让客户端将刷新令牌作为一个 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"; } }
您需要了解几件事:
我们在 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; } }
同样,这里有许多重要的实现要点:
最后,让我们修改前端应用,并使用令牌刷新:
以下是我们的函数 refreshAccessToken() :
$scope.refreshAccessToken = function(){ obtainAccessToken($scope.refreshData); }
以及 $scope.refreshData :
$scope.refreshData = {grant_type:"refresh_token"};
请注意,我们简单地使用了现有的 getAccessToken 函数 — 只是传入的参数不同。
还要注意的是,我们没有添加 refresh_token ,因为这属于 Zuul 过滤器负责。
在此 OAuth 教程中,我们学习了如何在 AngularJS 客户端应用中存储 Refresh Token、如何刷新过期的 Access Token 以及如何利用 Zuul 代理这些工作。
本教程的完整实现可以在 项目 github 中找到 — 这是一个基于 Eclipse 的项目,因此应该很容易导入和运行。