Spring Security с JWT для REST API

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

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

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

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

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

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

Аутентификация – это процесс проверки личности пользователя на основе предоставленных учетных данных. Обычный пример – ввод имени пользователя и пароля при входе на сайт. Это можно рассматривать как ответ на вопрос “Кто ты?
Авторизация относится к процессу определения того, имеет ли пользователь разрешение на выполнение определенного действия или чтение определенных данных, предполагая, что пользователь успешно прошел аутентификацию. По сути, это ответ на вопрос: “Может ли пользователь сделать или прочитать это?
Принцип относится к текущему аутентифицированному пользователю.
Предоставленные полномочия относятся к разрешению аутентифицированного пользователя.
Роль относится к группе разрешений аутентифицированного пользователя.

– Создание базового приложения 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 => Здравствуйте, админ.

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

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>
Войдите в полноэкранный режим Выход из полноэкранного режима

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

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

Using generated security password: 1fc10045-dfaa-4baq-a119-e32ez32c99ez
Войдите в полноэкранный режим Выход из полноэкранного режима

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

spring.security.user.password=Passer123@
Войдите в полноэкранный режим Выход из полноэкранного режима

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

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

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

Обзор архитектуры Spring Security

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

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

– Архитектура безопасности Spring

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

Пружина Защитная фильтрующая цепь

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

Например:

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

spring.security.filter.order=10
Войдите в полноэкранный режим Выход из полноэкранного режима

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

AuthenticationManager

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

AuthenticationProvider

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

  • authenticate выполняет аутентификацию запроса.

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

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

UserDetailsService

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

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

loadUserByUsername принимает имя пользователя в качестве параметра и возвращает объект идентификации пользователя.
Аутентификация с помощью JWT в Spring Security

Обсудив внутреннее устройство фреймворка Spring Security, давайте настроим его для аутентификации без статических данных с помощью токена 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
    }

}
Войдите в полноэкранный режим Выход из полноэкранного режима

В нашем примере приложения мы храним идентификаторы пользователей в базе данных H2, в коллекции 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 в виде боба.

В нашем примере проекта мы будем использовать алгоритм хэширования паролей 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.
  • Установите управление сеансами в режим stateless.
  • Установите обработчик исключений для неавторизованных запросов.
  • Установите разрешения на конечных точках.
  • Добавьте фильтр для токенов 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 перед внутренним Spring Security UsernamePasswordAuthenticationFilter. Мы делаем это потому, что на данном этапе нам нужен доступ к идентификатору пользователя для выполнения аутентификации/авторизации, а его извлечение осуществляется в фильтре 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-функцию входа в систему, нам нужно позаботиться еще об одном шаге: нам нужен доступ к менеджеру аутентификации. По умолчанию он не является общедоступным, и нам нужно явно раскрыть его как боб в нашем классе конфигурации.

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

@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 позволяет нам управлять пользователями приложения.
PARTNER_ADMIN позволяет нам управлять поставщиками.
PRODUCT_ADMIN позволяет нам управлять продуктами.
Теперь нам нужно применить их к соответствующим URL-адресам:

api/public является общедоступным.
api/admin/user могут получить доступ пользователи с ролью USER_ADMIN.
api/partner могут получить доступ пользователи с ролью PARTNER_ADMIN.
api/product могут получить доступ пользователи с ролью PRODUCT_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/partner/**").permitAll()
            .antMatchers(HttpMethod.POST, "/api/partner/search").permitAll()
            .antMatchers(HttpMethod.GET, "/api/product/**").permitAll()
            .antMatchers(HttpMethod.POST, "/api/product/search").permitAll()
            // Our private endpoints
            .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN)
            .antMatchers("/api/partner/**").hasRole(Role.PARTNER_ADMIN)
            .antMatchers("/api/product/**").hasRole(Role.PRODUCT_ADMIN)
            .anyRequest().authenticated();

        // Add JWT token filter
        http.addFilterBefore(
            jwtTokenFilter,
            UsernamePasswordAuthenticationFilter.class
        );
    }

    // Details omitted for brevity

}
Войдите в полноэкранный режим Выход из полноэкранного режима

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

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

@PreAuthorize поддерживает язык выражений Spring и используется для обеспечения контроля доступа на основе выражений перед выполнением метода.
@PostAuthorize поддерживает язык выражений Spring и используется для обеспечения контроля доступа на основе выражений после выполнения метода (предоставляет возможность доступа к результату метода).
@PreFilter поддерживает язык выражений Spring и используется для фильтрации коллекции или массивов перед выполнением метода на основе определенных нами пользовательских правил безопасности.
@PostFilter поддерживает язык выражений Spring и используется для фильтрации коллекции или массивов, возвращаемых после выполнения метода, в соответствии с определенными нами правилами безопасности (обеспечивает возможность доступа к результату метода).
@Secured не поддерживает Spring Expression Language и используется для указания списка ролей для метода.
@RolesAllowed не поддерживает Spring Expression Language и является аннотацией 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 = "Partner")
@RestController @RequestMapping(path = "api/partner")
public class AuthorApi {

    // Details omitted for brevity

    @RolesAllowed(Role.PARTNER_ADMIN)
    @PostMapping
    public void create() { }

    @RolesAllowed(Role.PARTNER_ADMIN)
    @PutMapping("{id}")
    public void edit() { }

    @RolesAllowed(Role.PARTNER_ADMIN)
    @DeleteMapping("{id}")
    public void delete() { }

    @GetMapping("{id}")
    public void get() { }

    @GetMapping("{id}/product")
    public void getProducts() { }

    @PostMapping("search")
    public void search() { }

}

@Api(tags = "Product")
@RestController @RequestMapping(path = "api/product")
public class BookApi {

    // Details omitted for brevity

    @RolesAllowed(Role.PRODUCT_ADMIN)
    @PostMapping
    public BookView create() { }

    @RolesAllowed(Role.PRODUCT_ADMIN)
    @PutMapping("{id}")
    public void edit() { }

    @RolesAllowed(Role.PRODUCT_ADMIN)
    @DeleteMapping("{id}")
    public void delete() { }

    @GetMapping("{id}")
    public void get() { }

    @GetMapping("{id}/partner")
    public void getPartners() { }

    @PostMapping("search")
    public void search() { }

}
Войдите в полноэкранный режим Выход из полноэкранного режима

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

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

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

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

Структура Spring Security различает два термина:

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

Власть @PreAuthorize(“hasAuthority(‘EDIT_PRODUCT’)”)
Роль @PreAuthorize(“hasRole(‘PRODUCT_ADMIN’)”)
Чтобы сделать разницу между этими двумя терминами более явной, фреймворк Spring Security по умолчанию добавляет к имени роли префикс ROLE_. Поэтому вместо проверки наличия роли с именем PRODUCT_ADMIN будет проверяться наличие ROLE_PRODUCT_ADMIN.

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

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // Details omitted for brevity

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
    }

}
Войдите в полноэкранный режим Выход из полноэкранного режима

Оцените статью
Procodings.ru
Добавить комментарий