Spring Security с JWT для REST API

Опубликовано: 2022-03-11

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

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

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

Определение терминологии

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

Вот термины, которые нам нужно рассмотреть:

  • Аутентификация относится к процессу проверки личности пользователя на основе предоставленных учетных данных. Типичным примером является ввод имени пользователя и пароля при входе на веб-сайт. Вы можете думать об этом как об ответе на вопрос Кто вы? .
  • Авторизация относится к процессу определения того, имеет ли пользователь надлежащее разрешение на выполнение определенного действия или чтение определенных данных, при условии, что пользователь успешно аутентифицирован. Вы можете думать об этом как об ответе на вопрос « Может ли пользователь это сделать/прочитать? » .
  • Принцип относится к текущему аутентифицированному пользователю.
  • Предоставленные полномочия относятся к разрешению аутентифицированного пользователя.
  • Роль относится к группе разрешений аутентифицированного пользователя.

Создание базового приложения Spring

Прежде чем перейти к настройке фреймворка Spring Security, давайте создадим базовое веб-приложение Spring. Для этого мы можем использовать Spring Initializr и создать шаблон проекта. Для простого веб-приложения достаточно только зависимости веб-фреймворка Spring:

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

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

 @RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }

После этого, если мы создадим и запустим проект, мы сможем получить доступ к следующим URL-адресам в веб-браузере:

  • http://localhost:8080/hello/user вернет строку Hello User .
  • http://localhost:8080/hello/admin вернет строку Hello Admin .

Теперь мы можем добавить инфраструктуру Spring Security в наш проект, и мы можем сделать это, добавив следующую зависимость в наш файл pom.xml :

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>

Добавление других зависимостей среды Spring обычно не оказывает немедленного влияния на приложение, пока мы не предоставим соответствующую конфигурацию, но Spring Security отличается тем, что имеет немедленный эффект, и это обычно сбивает с толку новых пользователей. После его добавления, если мы перестроим и запустим проект, а затем попытаемся получить доступ к одному из вышеупомянутых URL-адресов вместо просмотра результата, мы будем перенаправлены на http://localhost:8080/login . Это поведение по умолчанию, поскольку среда Spring Security требует аутентификации по умолчанию для всех URL-адресов.

Чтобы пройти аутентификацию, мы можем использовать имя пользователя по умолчанию user и найти автоматически сгенерированный пароль в нашей консоли:

 Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce

Помните, что пароль меняется каждый раз, когда мы перезапускаем приложение. Если мы хотим изменить это поведение и сделать пароль статическим, мы можем добавить следующую конфигурацию в наш файл application.properties :

 spring.security.user.password=Test12345_

Теперь, если мы введем учетные данные в форму входа, мы будем перенаправлены обратно на наш URL-адрес и увидим правильный результат. Обратите внимание, что стандартный процесс аутентификации основан на сеансе, и если мы хотим выйти из системы, мы можем получить доступ к следующему URL-адресу: http://localhost:8080/logout

Это готовое поведение может быть полезно для классических веб-приложений MVC, где у нас есть аутентификация на основе сеанса, но в случае одностраничных приложений это обычно бесполезно, потому что в большинстве случаев у нас есть клиентская сторона. рендеринг и аутентификация без сохранения состояния на основе JWT. В этом случае нам придется серьезно настроить инфраструктуру Spring Security, что мы и сделаем в оставшейся части статьи.

В качестве примера мы реализуем классическое веб-приложение книжного магазина и создадим серверную часть, которая будет предоставлять API CRUD для создания авторов и книг, а также API для управления пользователями и аутентификации.

Обзор архитектуры безопасности Spring

Прежде чем мы начнем настраивать конфигурацию, давайте сначала обсудим, как аутентификация Spring Security работает за кулисами.

На следующей диаграмме представлен поток и показано, как обрабатываются запросы аутентификации:

Весенняя архитектура безопасности

Весенняя архитектура безопасности

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

Сеть фильтров безопасности Spring

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

Например:

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

Одна важная деталь, о которой я хочу упомянуть, заключается в том, что фильтры Spring Security регистрируются в самом низком порядке и вызываются первыми. В некоторых случаях, если вы хотите поместить свой собственный фильтр перед ними, вам нужно будет добавить отступы к их порядку. Это можно сделать с помощью следующей конфигурации:

 spring.security.filter.order=10

Как только мы добавим эту конфигурацию в наш файл application.properties , у нас будет место для 10 пользовательских фильтров перед фильтрами Spring Security.

Менеджер аутентификации

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

AuthenticationProvider

AuthenticationProvider обрабатывает определенные типы аутентификации. Его интерфейс предоставляет только две функции:

  • authenticate выполняет аутентификацию с запросом.
  • supports проверяет, поддерживает ли данный провайдер указанный тип аутентификации.

Одной из важных реализаций интерфейса, который мы используем в нашем примере проекта, является DaoAuthenticationProvider , который извлекает сведения о пользователе из UserDetailsService .

UserDetailsService

UserDetailsService описывается как основной интерфейс, который загружает пользовательские данные в документации Spring.

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

  • loadUserByUsername принимает имя пользователя в качестве параметра и возвращает объект идентификации пользователя.

Аутентификация с использованием JWT с Spring Security

После обсуждения внутреннего устройства Spring Security framework давайте настроим его для аутентификации без сохранения состояния с помощью токена JWT.

Чтобы настроить Spring Security, нам нужен класс конфигурации, аннотированный аннотацией @EnableWebSecurity в нашем пути к классам. Кроме того, чтобы упростить процесс настройки, платформа предоставляет класс WebSecurityConfigurerAdapter . Мы расширим этот адаптер и переопределим обе его функции, чтобы:

  1. Настройте диспетчер аутентификации с правильным поставщиком
  2. Настройка веб-безопасности (общедоступные URL-адреса, частные URL-адреса, авторизация и т. д.)
 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }

В нашем примере приложения мы храним идентификаторы пользователей в базе данных MongoDB, в коллекции users . Эти удостоверения сопоставляются сущностью User , а их операции CRUD определяются репозиторием UserRepo Spring Data.

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

 public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }

Здесь мы видим, что требуется вернуть объект, который реализует интерфейс UserDetails , и наша сущность User реализует его (детали реализации см. в репозитории примера проекта). Учитывая тот факт, что он предоставляет только прототип с одной функцией, мы можем рассматривать его как функциональный интерфейс и предоставлять реализацию в виде лямбда-выражения.

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } // Details omitted for brevity }

Здесь вызов функции auth.userDetailsService инициирует экземпляр DaoAuthenticationProvider с использованием нашей реализации интерфейса UserDetailsService и зарегистрирует его в диспетчере аутентификации.

Наряду с провайдером проверки подлинности нам необходимо настроить диспетчер проверки подлинности с правильной схемой кодирования пароля, которая будет использоваться для проверки учетных данных. Для этого нам нужно предоставить предпочтительную реализацию интерфейса PasswordEncoder в виде bean-компонента.

В нашем примере проекта мы будем использовать алгоритм хеширования паролей bcrypt.

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Details omitted for brevity }

Настроив диспетчер аутентификации, теперь нам нужно настроить веб-безопасность. Мы внедряем REST API и нуждаемся в аутентификации без сохранения состояния с токеном JWT; поэтому нам нужно установить следующие параметры:

  • Включите CORS и отключите CSRF.
  • Установите управление сеансом без сохранения состояния.
  • Установить обработчик исключений несанкционированных запросов.
  • Установите разрешения на конечных точках.
  • Добавьте фильтр токенов JWT.

Эта конфигурация реализована следующим образом:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; private final JwtTokenFilter jwtTokenFilter; public SecurityConfig(UserRepo userRepo, JwtTokenFilter jwtTokenFilter) { this.userRepo = userRepo; this.jwtTokenFilter = jwtTokenFilter; } // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Used by spring security if CORS is enabled. @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

Обратите внимание, что мы добавили JwtTokenFilter перед внутренним UsernamePasswordAuthenticationFilter Spring Security. Мы делаем это, потому что на данном этапе нам нужен доступ к удостоверению пользователя для выполнения аутентификации/авторизации, и его извлечение происходит внутри фильтра токена JWT на основе предоставленного токена JWT. Это реализовано следующим образом:

 @Component public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserRepo userRepo; public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserRepo userRepo) { this.jwtTokenUtil = jwtTokenUtil; this.userRepo = userRepo; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Get authorization header and validate final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith("Bearer ")) { chain.doFilter(request, response); return; } // Get jwt token and validate final String token = header.split(" ")[1].trim(); if (!jwtTokenUtil.validate(token)) { chain.doFilter(request, response); return; } // Get user identity and set it on the spring security context UserDetails userDetails = userRepo .findByUsername(jwtTokenUtil.getUsername(token)) .orElse(null); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails == null ? List.of() : userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }

Прежде чем реализовать нашу функцию API входа в систему, нам нужно позаботиться еще об одном шаге — нам нужен доступ к диспетчеру аутентификации. По умолчанию он недоступен публично, и нам нужно явно представить его как bean-компонент в нашем классе конфигурации.

Это можно сделать следующим образом:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

И теперь мы готовы реализовать нашу функцию API входа в систему:

 @Api(tags = "Authentication") @RestController @RequestMapping(path = "api/public") public class AuthApi { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; private final UserViewMapper userViewMapper; public AuthApi(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserViewMapper userViewMapper) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; this.userViewMapper = userViewMapper; } @PostMapping("login") public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) { try { Authentication authenticate = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); User user = (User) authenticate.getPrincipal(); return ResponseEntity.ok() .header( HttpHeaders.AUTHORIZATION, jwtTokenUtil.generateAccessToken(user) ) .body(userViewMapper.toUserView(user)); } catch (BadCredentialsException ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } }

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

Авторизация с помощью Spring Security

В предыдущем разделе мы настроили процесс аутентификации и настроили общедоступные/частные URL-адреса. Этого может быть достаточно для простых приложений, но для большинства реальных случаев использования нам всегда нужны политики доступа на основе ролей для наших пользователей. В этой главе мы рассмотрим эту проблему и настроим схему авторизации на основе ролей с использованием среды Spring Security.

В нашем примере приложения мы определили следующие три роли:

  • USER_ADMIN позволяет нам управлять пользователями приложения.
  • AUTHOR_ADMIN позволяет нам управлять авторами.
  • BOOK_ADMIN позволяет нам управлять книгами.

Теперь нам нужно применить их к соответствующим URL-адресам:

  • api/public общедоступен.
  • api/admin/user может получить доступ к пользователям с ролью USER_ADMIN .
  • api/author может получить доступ к пользователям с ролью AUTHOR_ADMIN .
  • api/book может получить доступ к пользователям с ролью BOOK_ADMIN .

Платформа Spring Security предоставляет нам два варианта настройки схемы авторизации:

  • Конфигурация на основе URL
  • Конфигурация на основе аннотаций

Во-первых, давайте посмотрим, как работает конфигурация на основе URL. Его можно применить к конфигурации веб-безопасности следующим образом:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN) .antMatchers("/api/author/**").hasRole(Role.AUTHOR_ADMIN) .antMatchers("/api/book/**").hasRole(Role.BOOK_ADMIN) .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Details omitted for brevity }

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

Платформа Spring Security определяет следующие аннотации для веб-безопасности:

  • @PreAuthorize поддерживает язык выражений Spring и используется для обеспечения контроля доступа на основе выражений перед выполнением метода.
  • @PostAuthorize поддерживает Spring Expression Language и используется для обеспечения управления доступом на основе выражений после выполнения метода (обеспечивает возможность доступа к результату метода).
  • @PreFilter поддерживает язык выражений Spring и используется для фильтрации коллекции или массивов перед выполнением метода на основе определяемых нами пользовательских правил безопасности.
  • @PostFilter поддерживает язык выражений Spring и используется для фильтрации возвращаемой коллекции или массивов после выполнения метода на основе определяемых нами пользовательских правил безопасности (обеспечивает возможность доступа к результату метода).
  • @Secured не поддерживает язык выражений Spring и используется для указания списка ролей в методе.
  • @RolesAllowed не поддерживает язык выражений Spring и является эквивалентной аннотацией JSR 250 для аннотации @Secured .

Эти аннотации отключены по умолчанию и могут быть включены в нашем приложении следующим образом:

 @EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }


securedEnabled = true включает аннотацию @Secured .
jsr250Enabled = true включает аннотацию @RolesAllowed .
prePostEnabled = true включает @PreAuthorize , @PostAuthorize , @PreFilter , @PostFilter .

После их включения мы можем применять политики доступа на основе ролей на наших конечных точках API следующим образом:

 @Api(tags = "UserAdmin") @RestController @RequestMapping(path = "api/admin/user") @RolesAllowed(Role.USER_ADMIN) public class UserAdminApi { // Details omitted for brevity } @Api(tags = "Author") @RestController @RequestMapping(path = "api/author") public class AuthorApi { // Details omitted for brevity @RolesAllowed(Role.AUTHOR_ADMIN) @PostMapping public void create() { } @RolesAllowed(Role.AUTHOR_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.AUTHOR_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/book") public void getBooks() { } @PostMapping("search") public void search() { } } @Api(tags = "Book") @RestController @RequestMapping(path = "api/book") public class BookApi { // Details omitted for brevity @RolesAllowed(Role.BOOK_ADMIN) @PostMapping public BookView create() { } @RolesAllowed(Role.BOOK_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.BOOK_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/author") public void getAuthors() { } @PostMapping("search") public void search() { } }

Обратите внимание, что аннотации безопасности могут быть предоставлены как на уровне класса, так и на уровне метода.

Продемонстрированные примеры просты и не представляют реальных сценариев, но Spring Security предоставляет богатый набор аннотаций, и вы можете работать со сложной схемой авторизации, если решите их использовать.

Префикс имени роли по умолчанию

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

Фреймворк Spring Security различает два термина:

  • Authority представляют собой индивидуальное разрешение.
  • Role представляет группу разрешений.

Оба могут быть представлены с помощью одного интерфейса с именем GrantedAuthority а затем проверены с помощью Spring Expression Language внутри аннотаций Spring Security следующим образом:

  • Authority : @PreAuthorize («имеет полномочия ('EDIT_BOOK')»)
  • Role : @PreAuthorize("hasRole('BOOK_ADMIN')")

Чтобы сделать разницу между этими двумя терминами более явной, платформа Spring Security по умолчанию добавляет префикс ROLE_ к имени роли. Таким образом, вместо проверки роли с именем BOOK_ADMIN будет проверяться ROLE_BOOK_ADMIN .

Лично меня такое поведение сбивает с толку, и я предпочитаю отключать его в своих приложениях. Его можно отключить в конфигурации Spring Security следующим образом:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }

Тестирование с помощью Spring Security

Чтобы протестировать наши конечные точки с помощью модульных или интеграционных тестов при использовании среды Spring Security, нам нужно добавить зависимость spring-security-test вместе с spring-boot-starter-test . Наш файл сборки pom.xml будет выглядеть так:

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>

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

Эти аннотации:

  • @WithMockUser можно добавить в тестовый метод, чтобы эмулировать работу с имитируемым пользователем.
  • @WithUserDetails можно добавить в тестовый метод для имитации работы с UserDetails , возвращаемыми из UserDetailsService .
  • @WithAnonymousUser можно добавить в тестовый метод для имитации работы с анонимным пользователем. Это полезно, когда пользователь хочет запустить большинство тестов от имени конкретного пользователя и переопределить несколько методов, чтобы они были анонимными.
  • @WithSecurityContext определяет, какой SecurityContext использовать, и все три описанные выше аннотации основаны на нем. Если у нас есть конкретный вариант использования, мы можем создать собственную аннотацию, которая использует @WithSecurityContext для создания любого желаемого SecurityContext . Его обсуждение выходит за рамки нашей статьи, и, пожалуйста, обратитесь к документации Spring Security для получения дополнительной информации.

Самый простой способ запустить тесты с конкретным пользователем — использовать аннотацию @WithMockUser . Мы можем создать с ним фиктивного пользователя и запустить тест следующим образом:

 @Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }

Однако у этого подхода есть пара недостатков. Во-первых, фиктивный пользователь не существует, и если вы запустите интеграционный тест, который позже запрашивает информацию о пользователе из базы данных, тест завершится ошибкой. Во-вторых, фиктивный пользователь является экземпляром класса org.springframework.security.core.userdetails.User , который является внутренней реализацией Spring Framework интерфейса UserDetails , и если у нас есть собственная реализация, это может вызвать конфликты позже, во время выполнение теста.

Если предыдущие недостатки блокируют наше приложение, то аннотация @WithUserDetails — это то, что вам нужно. Он используется, когда у нас есть пользовательские UserDetails и UserDetailsService . Предполагается, что пользователь существует, поэтому мы должны либо создать фактическую строку в базе данных, либо предоставить фиктивный экземпляр UserDetailsService перед запуском тестов.

Вот как мы можем использовать эту аннотацию:

 @Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }

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

Использование @WithAnonymousUser позволяет работать как анонимный пользователь. Это особенно удобно, когда вы хотите запустить большинство тестов с конкретным пользователем, но несколько тестов от имени анонимного пользователя. Например, следующий пример запускает тестовые случаи test1 и test2 с фиктивным пользователем и test3 с анонимным пользователем:

 @SpringBootTest @AutoConfigureMockMvc @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void test1() { // Details omitted for brevity } @Test public void test2() { // Details omitted for brevity } @Test @WithAnonymousUser public void test3() throws Exception { // Details omitted for brevity } }

Подведение итогов

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

В этой статье я постарался продемонстрировать все тонкости настройки, и надеюсь, примеры будут вам полезны. Полные примеры кода см. в репозитории Git моего примера проекта Spring Security.