Рубрики
Без рубрики

OAuth2 для Spring REST API – Обработайте токен обновления в AngularJS (legacy OAuth stack)

Мы узнали, как хранить токен обновления в клиентском приложении AngularJS, как обновить токен доступа с истекшим сроком действия и как использовать прокси-сервер Zuul.

Автор оригинала: baeldung.

1. Обзор

В этом уроке мы продолжим изучение потока паролей OAuth, который мы начали собирать в нашей предыдущей статье, и сосредоточимся на том, как обрабатывать токен обновления в приложении AngularJS.

Примечание : эта статья использует Spring OAuth legacy project . Для версии этой статьи, использующей новый стек Spring Security 5, ознакомьтесь с нашей статьей OAuth2 для Spring REST API – Обработайте токен обновления в 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 используется только для хранения и он не управляет ничем другим в потоке OAuth. Например, браузер никогда не будет автоматически отправлять файлы cookie на сервер с запросами.

Также обратите внимание, как мы на самом деле вызываем эту функцию get Access Token() :

$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

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

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

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” для обработки запроса перед его передачей.

5. Поместите маркер обновления в файл cookie

Перейдем к забавным вещам.

То, что мы планируем сделать здесь, – это заставить клиента получить маркер обновления в виде файла cookie. Не просто обычный файл cookie, а защищенный файл cookie HttpOnly с очень ограниченным путем ( /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 для чтения ответа и извлечения токена обновления
  • Мы удалили значение refresh_token из ответа JSON, чтобы убедиться, что оно никогда не будет доступно переднему концу вне файла cookie
  • Мы устанавливаем максимальный возраст файла cookie на 30 дней -так как это соответствует времени истечения срока действия токена

Чтобы добавить дополнительный уровень защиты от CSRF-атак, мы добавим заголовок файла cookie того же сайта ко всем нашим файлам cookie .

Для этого мы создадим класс конфигурации:

@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

Теперь, когда у нас есть токен обновления в файле 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 – используется для инъекции наших параметров токена обновления :

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
  • Если нет refresh Token cookie (либо истекший срок действия, либо первый логин) – то запрос токена доступа будет перенаправлен без каких-либо изменений

7. Обновление маркера доступа из AngularJS

Наконец, давайте изменим наше простое интерфейсное приложение и фактически воспользуемся обновлением токена:

Вот наша функция refresh Access Token() :

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

А вот наш $scope.refresh Data :

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

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

Также обратите внимание, что мы не добавляем refresh_token сами – так как об этом позаботится фильтр Zuul.

8. Заключение

В этом уроке OAuth мы узнали, как хранить токен обновления в клиентском приложении AngularJS, как обновить токен доступа с истекшим сроком действия и как использовать прокси-сервер Zuul для всего этого.

Полную реализацию этого учебника можно найти в проекте github .