OAuth2 für eine Spring-REST-API - Behandelt das Aktualisierungs-Token in AngularJS

OAuth2 für eine Spring-REST-API - Behandeln Sie das Aktualisierungstoken in AngularJS

1. Überblick

In diesem Tutorial werden wir den OAuth-Kennwortfluss, den wir in mehrour previous article zusammengestellt haben, weiter untersuchen und uns darauf konzentrieren, wie das Aktualisierungstoken in einer AngularJS-App behandelt wird.

2. Zugriff auf Token-Ablauf

Denken Sie zunächst daran, dass der Client ein Zugriffstoken erhalten hat, als sich der Benutzer bei der Anwendung anmeldete:

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";
        });
}

Beachten Sie, wie unser Zugriffstoken in einem Cookie gespeichert wird, das je nach Ablauf des Tokens abläuft.

Es ist wichtig zu verstehen, dassthe cookie itself is only used for storage ist und nichts anderes im OAuth-Fluss steuert. Beispielsweise sendet der Browser das Cookie niemals automatisch mit Anfragen an den Server.

Beachten Sie auch, wie wir dieseobtainAccessToken()-Funktion tatsächlich aufrufen:

$scope.loginData = {
    grant_type:"password",
    username: "",
    password: "",
    client_id: "fooClientIdPassword"
};

$scope.login = function() {
    obtainAccessToken($scope.loginData);
}

3. Der Proxy

In der Front-End-Anwendung wird jetzt ein Zuul-Proxy ausgeführt, der sich im Wesentlichen zwischen dem Front-End-Client und dem Autorisierungsserver befindet.

Konfigurieren wir die Routen des Proxys:

zuul:
  routes:
    oauth:
      path: /oauth/**
      url: http://localhost:8081/spring-security-oauth-server/oauth

Interessant ist hier, dass wir nur Datenverkehr an den Autorisierungsserver weiterleiten und nichts anderes. Wir brauchen den Proxy nur dann wirklich, wenn der Client neue Token erhält.

Wenn Sie die Grundlagen von Zuul erläutern möchten, lesen Sie kurzthe main Zuul article.

4. Ein Zuul-Filter für die Standardauthentifizierung

Die erste Verwendung des Proxys ist einfach. Anstatt unsere App "client secret" in Javascript anzuzeigen, verwenden wir einen Zuul-Vorfilter, um einen Autorisierungsheader für den Zugriff auf Token-Anforderungen hinzuzufügen:

@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";
    }
}

Beachten Sie nun, dass dies keine zusätzliche Sicherheit bietet. Der einzige Grund, warum wir dies tun, besteht darin, dass der Token-Endpunkt mit der Basisauthentifizierung mithilfe von Client-Anmeldeinformationen gesichert wird.

Aus Sicht der Implementierung ist der Filtertyp besonders hervorzuheben. Wir verwenden einen Filtertyp von "pre", um die Anforderung zu verarbeiten, bevor sie weitergeleitet wird.

Auf zu den lustigen Sachen.

Wir planen hier, dass der Client das Aktualisierungstoken als Cookie erhält. Nicht nur ein normales Cookie, sondern ein gesichertes Nur-HTTP-Cookie mit einem sehr begrenzten Pfad (/oauth/token).

Wir richten einen Zuul-Nachfilter ein, um das Aktualisierungstoken aus dem JSON-Hauptteil der Antwort zu extrahieren und im Cookie festzulegen:

@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>() {});
                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";
    }
}

Ein paar interessante Dinge, die Sie hier verstehen sollten:

  • Wir haben einen Zuul-Nachfilter verwendet, um die Antwort undextract refresh token zu lesen

  • Wir haben den Wert vonrefresh_token aus der JSON-Antwort entfernt, um sicherzustellen, dass das Front-End außerhalb des Cookies niemals darauf zugreifen kann

  • Wir setzen das maximale Alter des Cookies auf30 days - da dies der Ablaufzeit des Tokens entspricht

Wenn wir nun das Aktualisierungstoken im Cookie haben und die AngularJS-Front-End-Anwendung versucht, eine Tokenaktualisierung auszulösen, wird die Anforderung mit/oauth/token gesendet, und der Browser sendet dieses Cookie natürlich.

Wir haben jetzt einen weiteren Filter im Proxy, der das Aktualisierungstoken aus dem Cookie extrahiert und als HTTP-Parameter weiterleitet - damit die Anforderung gültig ist:

public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    ...
    HttpServletRequest req = ctx.getRequest();
    String refreshToken = extractRefreshToken(req);
    if (refreshToken != null) {
        Map param = new HashMap();
        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;
}

Und hier ist unserCustomHttpServletRequest - gewöhnt aninject our refresh token parameters:

public class CustomHttpServletRequest extends HttpServletRequestWrapper {
    private Map additionalParams;
    private HttpServletRequest request;

    public CustomHttpServletRequest(
      HttpServletRequest request, Map additionalParams) {
        super(request);
        this.request = request;
        this.additionalParams = additionalParams;
    }

    @Override
    public Map getParameterMap() {
        Map map = request.getParameterMap();
        Map param = new HashMap();
        param.putAll(map);
        param.putAll(additionalParams);
        return param;
    }
}

Auch hier einige wichtige Hinweise zur Implementierung:

  • Der Proxy extrahiert das Aktualisierungstoken aus dem Cookie

  • Anschließend wird es in den Parameterrefresh_tokengesetzt

  • Außerdem werdengrant_type aufrefresh_token gesetzt

  • Wenn keinrefreshToken-Cookie vorhanden ist (entweder abgelaufen oder erste Anmeldung), wird die Zugriffstoken-Anforderung ohne Änderung umgeleitet

7. Aktualisieren des Zugriffstokens von AngularJS

Lassen Sie uns abschließend unsere einfache Front-End-Anwendung ändern und das Token tatsächlich aktualisieren:

Hier ist unsere FunktionrefreshAccessToken():

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

Und hier unsere$scope.refreshData:

$scope.refreshData = {grant_type:"refresh_token"};

Beachten Sie, dass wir einfach die vorhandeneobtainAccessToken-Funktion verwenden und nur verschiedene Eingaben an sie übergeben.

Beachten Sie auch, dass wir dierefresh_tokennicht selbst hinzufügen, da dies vom Zuul-Filter erledigt wird.

8. Fazit

In diesem OAuth-Lernprogramm haben wir gelernt, wie das Aktualisierungstoken in einer AngularJS-Clientanwendung gespeichert wird, wie ein abgelaufenes Zugriffstoken aktualisiert wird und wie der Zuul-Proxy für all dies verwendet wird.

Diefull implementation dieses Tutorials finden Sie inthe github project - dies ist ein Eclipse-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.