Spring REST API用のOAuth2 - AngularJSの更新トークンを処理する

1概要

このチュートリアルでは、まとめて始めたOAuthパスワードフローを引き続き検討します:/rest-api-spring-oauth2-angularjs AngularJSアプリ。

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自体は保存用にのみ使用され、OAuthフローの他の部分を促進するものではないということです。たとえば、ブラウザがリクエストとともに自動的にクッキーをサーバーに送信することはありません。

また、実際にこの obtainAccessToken() 関数を呼び出す方法にも注意してください。

$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の基本を学びたいのなら、 Zuulのメインの記事 を読んでください。

4基本認証を行うZuulフィルタ

プロキシの最初の使い方は簡単です - 私たちのアプリ“ client secret ”をJavaScriptで公開する代わりに、トークンリクエストにアクセスするためのAuthorizationヘッダを追加するために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";
    }
}

ただし、これによってセキュリティが強化されるわけではなく、トークンのエンドポイントがクライアント認証情報を使用した基本認証で保護されているためです。

実装の観点からは、フィルタの種類は特に注目に値します。リクエストを渡す前に処理するために、フィルタタイプ「pre」を使用しています。

5リフレッシュトークンをクッキーに入れる

楽しいものに。

ここで計画しているのは、クライアントに更新トークンをCookieとして取得させることです。通常のCookieだけでなく、非常に制限されたパス( /oauth/token )を持つセキュアなHTTP専用のcookie。

レスポンスのJSON本文からRefresh Tokenを抽出してcookieに設定するZuulポストフィルタを設定します。

@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ポストフィルタを使用し、リフレッシュを抽出

トークン makeのJSONレスポンスから refresh token__の値を削除しました

Cookieの外側のフロントエンドにアクセスできないようにする クッキーの最大保存期間を 30日** に設定します。

トークンの有効期限

6. Cookieからリフレッシュトークンを取得して使用する

CookieにRefresh Tokenがあるので、フロントエンドのAngular JSアプリケーションがトークンの更新を起動しようとすると、 /oauth/token にリクエストが送信され、ブラウザはもちろんその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__パラメータに設定します

  • また、 grant type refresh token に設定しています

  • refreshToken cookieがない場合(期限切れまたは最初のログイン) -

その後、アクセストークン要求は変更されずにリダイレクトされます。

7. AngularJS からのアクセストークンの更新

最後に、単純なフロントエンドアプリケーションを修正して、実際にトークンの更新を利用しましょう。

これが私たちの関数 refreshAccessToken() です。

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

そしてここで私たちの $ scope.refreshData :

$scope.refreshData = {grant__type:"refresh__token"};

既存の obtainAccessToken 関数を単純に使用していることと、さまざまな入力を渡していることに注目してください。

また、 refresh token__を自分自身で追加しているのではないことにも注意してください。これはZuulフィルタによって処理されます。

8結論

このOAuthチュートリアルでは、更新トークンをAngularJSクライアントアプリケーションに格納する方法、有効期限が切れたアクセストークンを更新する方法、およびそのすべてにZuulプロキシを利用する方法を学びました。

このチュートリアルの 完全な実装 はhttps://github.com/eugenp/spring-security-oauth/[the github project]にあります - これはEclipseベースのプロジェクトなので、インポートと実行が簡単です。そのまま。