OAuth2 для API Spring REST - обрабатывать токен обновления в AngularJS

OAuth2 для API Spring REST - обработка маркера обновления в AngularJS

1. обзор

В этом руководстве мы продолжим изучение потока паролей OAuth, который мы начали собирать в несколькихour previous article, и сосредоточимся на том, как обрабатывать токен обновления в приложении 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, срок действия которого истекает в зависимости от срока действия самого токена.

Важно понимать, чтоthe cookie itself is only used for storage, и он больше ни на что не влияет в потоке 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

Интересно то, что мы только проксируем трафик на сервер авторизации, а не на что-либо еще. Нам действительно нужен прокси, чтобы войти, когда клиент получает новые токены.

Если вы хотите познакомиться с основами Зуула, прочтите быстроthe main Zuul article.

4. Фильтр Zuul, который выполняет базовую аутентификацию

Первое использование прокси очень простое - вместо того, чтобы отображать наше приложение «client secret» в 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";
    }
}

Теперь имейте в виду, что это не добавляет дополнительной безопасности, и единственная причина, по которой мы это делаем, заключается в том, что конечная точка токена защищена с помощью базовой аутентификации с использованием учетных данных клиента.

С точки зрения реализации, тип фильтра особенно заслуживает внимания. Мы используем тип фильтра «pre», чтобы обработать запрос перед его передачей.

На веселье.

Мы планируем сделать так, чтобы клиент получил токен обновления в виде файла cookie. Не просто обычный файл cookie, а защищенный файл cookie только для HTTP с очень ограниченным путем (/oauth/token).

Мы настроим пост-фильтр Zuul для извлечения токена обновления из тела JSON-ответа и установки его в 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>() {});
                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 для чтения ответа иextract refresh token

  • Мы удалили значениеrefresh_token из ответа JSON, чтобы убедиться, что он никогда не будет доступен для внешнего интерфейса за пределами файла cookie.

  • Мы устанавливаем максимальный возраст файла cookie на30 days, поскольку он соответствует времени истечения срока действия токена.

Теперь, когда у нас есть токен обновления в файле cookie, когда интерфейсное приложение AngularJS пытается инициировать обновление токена, оно отправляет запрос на/oauth/token, и браузер, конечно же, отправит этот файл cookie.

Таким образом, теперь у нас будет другой фильтр в прокси-сервере, который будет извлекать токен обновления из файла cookie и отправлять его в качестве параметра HTTP, чтобы запрос был действительным:

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

А вот нашCustomHttpServletRequest - использованный дляinject 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;
    }
}

Опять же, здесь много важных замечаний по реализации:

  • Прокси-сервер извлекает из файла cookie токен обновления

  • Затем он устанавливает его в параметрrefresh_token

  • Он также устанавливаетgrant_type наrefresh_token

  • Если нет файла cookierefreshToken (срок действия истек или первый вход), то запрос токена доступа будет перенаправлен без изменений

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 для всего этого.

full implementation этого руководства можно найти вthe github project - это проект на основе Eclipse, поэтому его должно быть легко импортировать и запускать как есть.