REST API с использованием Spring Security и JWT / Хабр

Что вы создадите

Вы создадите Spring MVC приложение, которое обеспечивает защиту страницы входа фиксированным списком пользователей.

Технологии

Для решения используем фреймворк Spring Boot и Spring Web, для него требуется:

  1. Java 8 ;

  2. Apache Maven

Авторизация и валидация будет выполнена силами Spring Security и JsonWebToken (JWT).Для уменьшения кода использую Lombok.

1 Создание Web-проекта

Создаем Maven-проект SpringBootSecurityRest. При инициализации, если вы это делаете через Intellij IDEA, добавьте Spring Boot DevTools, Lombok и Spring Web, иначе добавьте зависимости отдельно в pom-файле.

2 Конфигурация pom-xml

После развертывания проекта pom-файл должен выглядеть следующим образом:

  1. Должен быть указан parent-сегмент с подключенным spring-boot-starter-parent;

  2. И установлены зависимости spring-boot-starter-web, spring-boot-devtools и Lombok.

3 Создание ресурса REST

Разделим все классы на слои, создадим в папке com.springbootsecurityrest четыре новые папки:

Spring Security

Простенькое REST API написано и пока оно открыто для всех. Двигаемся дальше, теперь его необходимо защитить, а доступ открыть только авторизованным пользователям. Для этого воспользуемся Spring Security и JWT.

Spring Security это Java/JavaEE framework, предоставляющий механизмы построения систем аутентификации и авторизации, а также другие возможности обеспечения безопасности для корпоративных приложений, созданных с помощью Spring Framework.

JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.

1 Подключаем зависимости

Добавляем новые зависимости в pom-файл.

2 Генерация и хранения токена

Начнем с генерации и хранения токена, для этого создадим папку security и в ней создаем класс JwtTokenRepository с имплементацией интерфейса CsrfTokenRepository (из пакета org.springframework.security.web.csrf).

Интерфейс указывает на необходимость реализовать три метода:

  1. Генерация токена в методе generateToken;

  2. Сохранения токена – saveToken;

  3. Получение токена – loadToken.

Генерируем токен силами Jwt, пример реализации метода.

3 Создание нового фильтра для SpringSecurity

Создаем новый класс JwtCsrfFilter, который является реализацией абстрактного класса OncePerRequestFilter (пакет org.springframework.web.filter). Класс будет выполнять валидацию токена и инициировать создание нового. Если обрабатываемый запрос относится к авторизации (путь /auth/login), то логика не выполняется и запрос отправляется далее для выполнения базовой авторизации.

6 Обработка ошибок

Что бы видеть ошибки авторизации или валидации токена, необходимо подготовить обработчик ошибок. Для этого создаем новый класс GlobalExceptionHandler в корне com.springbootsecurityrest, который является расширением класса ResponseEntityExceptionHandler с реализацией метода handleAuthenticationException.

Метод будет устанавливать статус ответа 401 (UNAUTHORIZED) и возвращать сообщение в формате ErrorInfo.

7 Настройка конфигурационного файла Spring Security.

Все данные подготовили и теперь необходимо настроить конфигурационный файл. В папке com.springbootsecurityrest создаем файл SpringSecurityConfig, который является реализацией абстрактного класса WebSecurityConfigurerAdapter пакета org.springframework.security.config.annotation.web.configuration. Помечаем класс двумя аннотациями: Configuration и EnableWebSecurity.

Authentication до аутентификации

Как видно в коде выше, метод authenticate() получает на вход объект Authentication с именем и паролем, полученными от клиента и требующими проверку. Имя хранится в principal, а пароль в credenticals (до проверки, после проверки будет иначе):

До проверки в выделенных полях хранятся имя и пароль
До проверки в выделенных полях хранятся имя и пароль

Содержимое объекта Authentication можно проверить, если запустить предыдущий пример и поставить break-point в методе authenticate() класса ProviderManager. А потом по адресу /login отправить POST-запрос с формы ввода имени/пароля:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
}

Найти класс ProviderManager — Ctrl N в IntelliJ IDEA

До аутентификации
До аутентификации

isAuthenticated() до аутентификации равно false.

Если аутентификация не прошла (имя и пароль неверны), то выбрасывается исключение BadCredentials.

В случае же успеха возвращается тоже объект Authentication,  но заполненный по-другому.

Combining application security rules with actuator rules

If you use the Spring Boot Actuator for management endpoints, you probably want them to be secure, and, by default, they are. In fact, as soon as you add the Actuator to a secure application, you get an additional filter chain that applies only to the actuator endpoints.

It is defined with a request matcher that matches only actuator endpoints and it has an order of ManagementServerProperties.BASIC_AUTH_ORDER, which is 5 fewer than the default SecurityProperties fallback filter, so it is consulted before the fallback.

If you want your application security rules to apply to the actuator endpoints, you can add a filter chain that is ordered earlier than the actuator one and that has a request matcher that includes all actuator endpoints. If you prefer the default security settings for the actuator endpoints, the easiest thing is to add your own filter later than the actuator one, but earlier than the fallback (for example, ManagementServerProperties.BASIC_AUTH_ORDER 1), as follows:

Maven-зависимость

Во-первых, это стартер для работы с базой через JPA:

Во-вторых,  In-Memory база данных H2 (она удобна для учебных примеров, поскольку не нужно ставить на компьютер реальную базу):

Securitycontext — хранилище объекта authentication

Допустим, аутентификация прошла успешно — это значит, имя и пароль верные.

Тогда объект Authentication сохраняется в SecurityContext, а тот, в свою очередь, — в SecurityContextHolder:

SecurityContextHolder
SecurityContextHolder

Текущего пользователя из него можно получить так:

Восстановление authentication из сессии

Аутентификация в нашем примере происходит только раз. Коль скоро она прошла успешно, authentication восстанавливается из контекста, а в итоге из сессии при последующих запросах. Происходит это в SecurityContextPersistenceFilter.

Как проходить этот урок

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

Чтобы начать с нуля, перейдите в Настройка проекта.

Чтобы пропустить базовые шаги, выполните следующее:

Когда вы закончите, можете сравнить получившийся результат с образцом в gs-securing-web/complete.

Настроим spring security.

Соберем все проделанное выше вместе и настроим Spring Security.

Настройка spring security

Предположим, что вы хотите предотвратить неавторизованный доступ к просмотру представления
“/hello”. Сейчас, если пользователь нажмет на ссылку на домашней странице, он увидит приветствие
без каких либо помех к нему.

Настройка проекта

Для начала вам необходимо настроить базовый скрипт сборки. Вы можете использовать любую систему сборки,
которая вам нравится для сборки проетов Spring, но в этом уроке рассмотрим код для работы с
Gradle и
Maven.

Немного теории

Аутентификация

Переопределим authenticationfailurehandler.

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

ExampleAuthenticationFailureHandler

public class ExampleAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;

    public ExampleAuthenticationFailureHandler(
            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {
        this.authorizationRequestRepository = requireNonNull(authorizationRequestRepository);
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        String targetUrl = getFailureUrl(request, exception);
        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

    private String getFailureUrl(HttpServletRequest request, AuthenticationException exception) {
        String targetUrl = getCookie(request, Cookies.REDIRECT_URI)
                .map(Cookie::getValue)
                .orElse(("/"));

        return UriComponentsBuilder.fromUriString(targetUrl)
                .queryParam("error", exception.getLocalizedMessage())
                .build().toUriString();
    }
}

Подготовка

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

Реализация


Мы реализуем REST-сервис, предоставляющий следующее API:

  • GET /auth/login — запустить процесс аутентификации пользователя.
  • POST /auth/token — запросить новую пару access/refresh токенов.
  • GET /api/repositories — получить список Bitbucket репозиториев текущего пользователя.

REST API с использованием Spring Security и JWT / Хабр

Высокоуровневая архитектура приложения.

Заметим, что поскольку приложение состоит из трех взаимодействующих компонентов, помимо того, что мы выполняем авторизацию запросов клиента к серверу, Bitbucket авторизует запросы сервера к нему. Мы не будем настраивать авторизацию методов по ролям, чтобы не делать пример сложнее. У нас есть только один API метод GET /api/repositories, вызывать который могут только аутентифицированные пользователи. Сервер может выполнять на Bitbucket любые операции, разрешенные при регистрации OAuth клиента.
REST API с использованием Spring Security и JWT / Хабр
Процесс регистрации OAuth клиента описан в предыдущей статье.

Для реализации мы будем использовать Spring Boot версии 2.2.2.RELEASE и Spring Security версии 5.2.1.RELEASE.

Репозиторий

Пропишем в репозитории метод, который понадобится в дальнейшем:

Сборка исполняемого jar

Вы можете собрать единый исполняемый JAR-файл, который содержит все необходимые зависимости,
классы и ресурсы. Это делает его легким в загрузке, версионировании и развертывании сервиса как
приложения на протяжении всего периода разработки, на различных средах и так далее.

./gradlew build

Затем вы можете запустить JAR-файл:

java -jar build/libs/gs-securing-web-0.1.0.jar

Если вы используете Maven, вы можете запустить приложение, используя mvn spring-boot:run,
либо вы можете собрать приложение с mvn clean package и запустить JAR примерно так:

java -jar target/gs-securing-web-0.1.0.jar

Если вы используете Gradle, вы можете запустить ваш сервис из командной строки:

./gradlew clean build && java -jar build/libs/gs-securing-web-0.1.0.jar

Если вы используете Maven, то можете запустить ваш сервис таким образом:
mvn clean package && java -jar target/gs-securing-web-0.1.0.jar.

Как вариант, вы можете запустить ваш сервис напрямую из Gradle примерно так:

./gradlew bootRun

С mvn – mvn spring-boot:run.

... app starts up ...

Создадим tokenauthenticationfilter.


Задача этого фильтра извлечь токен доступа из заголовка Authorization в случае его наличия, провалидировать его и инициализировать секьюрити контекст.

Создание незащищенного web приложения

До того, как применить защиту к вашему приложению, вам необходимо само приложение.
Шаги в этой главе освещают процесс создания очень простого web-приложения. Затем в следующей главе
вы защитите его с помощью Spring Security.

src/main/resources/templates/home.html


Как вы можете увидеть, это простое приложение включает ссылку на страницу “/hello”,
которая представлена ниже также как Thymeleaf шаблон.

src/main/resources/templates/hello.html

Приложение основано на Spring MVC. Т.о. вам необходимо настроить Spring MVC и контроллеры
представлений для отображения этих шаблонов. Ниже конфигурационный класс для настройки Spring
MVC в приложении.

src/main/java/hello/MvcConfig.java

package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}

Метод addViewControllers() (переопределение метода с таким же названием в
WebMvcConfigurerAdapter), добавляющий четыре контроллера. Двое из них настроены
на представление с именем “home”(home.html), другой настроен на “hello”.
Четвертый контроллер настроен на представление с названием “login”. Вы создадите это представление
в следующей главе.

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

Фундамент простого web-приложения создано, теперь вы можете добавить защиту к нему.

Создание структуры каталогов

В выбранном вами каталоге проекта создайте следующую структуру каталогов; к примеру,
командой mkdir -p src/main/java/hello для *nix систем:

└── src
    └── main
        └── java
            └── hello

Создание файла сборки gradle

Если вы посмотрите на pom.xml, вы найдете, что указана версия для maven-compiler-plugin.
В общем, это не рекомендуется делать. В данном случае он предназначен для решения проблем с нашей CI системы,
которая по умолчанию имеет старую(до Java 5) версию этого плагина.

build.gradle

Spring Boot gradle plugin
предоставляет множество удобных возможностей:

Ссылки

  • OAuth — Википедия
  • Spring Security Reference
  • JSON Web Token (JWT)
  • The OAuth 2.0 Authorization Framework

P.S.

Схема и данные

Наконец, заполним базу парой пользователей с помощью data.sql (этот файл надо положить в папку resources):

Типы аутентификации в spring security

Есть несколько стандартных типов хранения и извлечения пользователя, и за каждый из них отвечает свой AuthenticationProvider. AuthenticationManager делегирует провайдеру извлечь данные их хранилища. В Spring Security реализованы несколько стандартных провайдеров, все они задаются в методе configure():

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 
   @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                ....
    }
...
}

Итак, провайдеры:

А можно написать свой, так чаще всего и делают (тоже сделано в следующей статье).

Итоги

В тексте выше приводились примеры кода и ставились break-point приложения из статьи.

Создадим repositories endpoint.

То ради чего и нужна была аутентификация через OAuth2 и Bitbucket — возможность использовать Bitbucket API для доступа к своим ресурсам. Используем Bitbucket repositories API для получения списка репозиториев текущего пользователя.

Создадим login endpoint.

Для аутентификации пользователя мы по-прежнему используем OAuth2 с типом авторизации Authorization Code. Однако на предыдущем шаге мы заменили стандартный AuthenticationEntryPoint своей реализацией, поэтому нам нужен явный способ запустить процесс аутентификации.

Заключение

В этой статье рассмотрели один из примеров реализации REST приложения с Spring Security и JWT. Надеюсь данный вариант реализации кому то окажется полезным.

Полный код проекта выложен доступен на github

Похожее:  JAVA EE: Разработка web-приложения. JAAS. Session.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *