«1. Обзор

В этом кратком руководстве мы собираемся показать, как мы можем добавить функцию выхода из системы в приложение OAuth Spring Security.

Мы рассмотрим несколько способов сделать это. Сначала мы увидим, как выйти из системы нашего пользователя Keycloak из приложения OAuth, как описано в разделе «Создание REST API с помощью OAuth2», а затем с помощью прокси-сервера Zuul, который мы видели ранее.

Мы будем использовать стек OAuth в Spring Security 5. Если вы хотите использовать устаревший стек Spring Security OAuth, ознакомьтесь с этой предыдущей статьей: Выход из защищенного приложения OAuth (используя устаревший стек).

2. Выход из системы с помощью внешнего приложения

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

В нашем примере, согласно документации Keycloak, для выхода непосредственно из приложения браузера мы можем перенаправить браузер на http://auth-server/auth/realms/{realm-name}/protocol/openid- подключиться/выйти?redirect_uri=encodedRedirectUri.

Наряду с отправкой URI перенаправления нам также необходимо передать id_token_hint конечной точке выхода Keycloak. Это должно содержать закодированное значение id_token.

Вспомним, как мы сохраняли access_token, аналогично сохраним и id_token:

saveToken(token) {
  var expireDate = new Date().getTime() + (1000 * token.expires_in);
  Cookie.set("access_token", token.access_token, expireDate);
  Cookie.set("id_token", token.id_token, expireDate);
  this._router.navigate(['/']);
}

Важно отметить, что для получения ID Token в полезной нагрузке ответа сервера авторизации мы должны включить openid в параметр масштаба.

Теперь давайте посмотрим на процесс выхода из системы в действии.

Мы изменим нашу функцию выхода из системы в службе приложений:

logout() {
  let token = Cookie.get('id_token');
  Cookie.delete('access_token');
  Cookie.delete('id_token');
  let logoutURL = "http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/logout?
    id_token_hint=" + token + "&post_logout_redirect_uri=" + this.redirectUri;

  window.location.href = logoutURL;
}

Помимо перенаправления, нам также необходимо отказаться от токенов доступа и идентификатора, которые мы получили от сервера авторизации.

Следовательно, в приведенном выше коде мы сначала удалили токены, а затем перенаправили браузер на API выхода Keycloak.

Примечательно, что мы передали URI перенаправления как http://localhost:8089/ — тот, который мы используем во всем приложении, — поэтому мы окажемся на целевой странице после выхода из системы.

Удаление токенов доступа, идентификатора и обновления, соответствующих текущему сеансу, выполняется на стороне сервера авторизации. В этом случае наше браузерное приложение вообще не сохранило Refresh Token.

3. Выход из системы с помощью Zuul Proxy

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

Здесь мы увидим, как добавить функцию выхода из системы.

На этот раз мы будем использовать другой API Keycloak для выхода пользователя из системы. Мы будем вызывать POST в конечной точке выхода из системы, чтобы выйти из сеанса с помощью вызова без браузера, вместо перенаправления URL-адреса, которое мы использовали в предыдущем разделе.

3.1. Определить маршрут для выхода

Для начала давайте добавим еще один маршрут к прокси в нашем application.yml:

zuul:
  routes:
    //...
    auth/refresh/revoke:
      path: /auth/refresh/revoke/**
      sensitiveHeaders:
      url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/logout
    
    //auth/refresh route

По сути, мы добавили подмаршрут к уже существующему auth/refresh. Важно, чтобы мы добавили подмаршрут перед основным маршрутом, иначе Zuul всегда будет отображать URL-адрес основного маршрута.

Мы добавили подмаршрут вместо основного, чтобы иметь доступ к cookie-файлу refreshToken только для HTTP, который был настроен на очень ограниченный путь как /auth/refresh (и его подпути). Мы увидим, зачем нам нужен файл cookie, в следующем разделе.

3.2. POST для /logout сервера авторизации

Теперь давайте улучшим реализацию CustomPreZuulFilter, чтобы перехватить URL-адрес /auth/refresh/revoke и добавить необходимую информацию для передачи на сервер авторизации.

Параметры формы, необходимые для выхода, аналогичны параметрам запроса Refresh Token, за исключением того, что нет Grant_type:

@Component 
public class CustomPostZuulFilter extends ZuulFilter { 
    //... 
    @Override 
    public Object run() { 
        //...
        if (requestURI.contains("auth/refresh/revoke")) {
            String cookieValue = extractCookie(req, "refreshToken");
            String formParams = String.format("client_id=%s&client_secret=%s&refresh_token=%s", 
              CLIENT_ID, CLIENT_SECRET, cookieValue);
            bytes = formParams.getBytes("UTF-8");
        }
        //...
    }
}

Здесь мы просто извлекли файл cookie refreshToken и отправили требуемые formParams.

3.3. Удаление токена обновления

При отзыве токена доступа с использованием перенаправления выхода из системы, как мы видели ранее, связанный с ним токен обновления также становится недействительным сервером авторизации.

«Однако в этом случае файл cookie httpOnly останется установленным на клиенте. Учитывая, что мы не можем удалить его с помощью JavaScript, нам нужно удалить его со стороны сервера.

Для этого добавим в реализацию CustomPostZuulFilter, которая перехватывает URL-адрес /auth/refresh/revoke, чтобы удалить файл cookie refreshToken при обнаружении этого URL-адреса:

@Component
public class CustomPostZuulFilter extends ZuulFilter {
    //...
    @Override
    public Object run() {
        //...
        String requestMethod = ctx.getRequest().getMethod();
        if (requestURI.contains("auth/refresh/revoke")) {
            Cookie cookie = new Cookie("refreshToken", "");
            cookie.setMaxAge(0);
            ctx.getResponse().addCookie(cookie);
        }
        //...
    }
}

3.4. Удалить токен доступа из клиента Angular

Помимо отзыва токена обновления, файл cookie access_token также необходимо удалить со стороны клиента.

Давайте добавим в наш контроллер Angular метод, который очищает файл cookie access_token и вызывает отображение POST /auth/refresh/revoke:

logout() {
  let headers = new HttpHeaders({
    'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'});
  
  this._http.post('auth/refresh/revoke', {}, { headers: headers })
    .subscribe(
      data => {
        Cookie.delete('access_token');
        window.location.href = 'http://localhost:8089/';
        },
      err => alert('Could not logout')
    );
}

Эта функция будет вызываться при нажатии на кнопку выхода:

<a class="btn btn-default pull-right"(click)="logout()" href="#">Logout</a>

~ ~~ 4. Заключение

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

Полный исходный код примеров можно найти на GitHub.