spring-security6 Управляющий авторизацией решение на основе тела запроса

Вопрос или проблема

Ищу руководство, каким образом правильно поступить. У меня есть REST API, который имеет одну конечную точку для изменения паролей пользователя. Это разрешено на основании двух критериев:

  • пользователь пытается изменить свой собственный пароль
  • агент службы поддержки пытается изменить пароль другого пользователя

Кто бы ни был запрашивающим, сначала происходит аутентификация, затем осуществляется .access().

@Bean
SecurityFilterChain changePasswordSecurity(HttpSecurity httpSecurity,
                                           ObjectMapper objectMapper) throws Exception {
    return httpSecurity
            .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class)
            .authorizeHttpRequest(r -> r
                    .requestMatchers(httpMethod.PUT, "/management/changePassword").access((authentication, object) -> {
                            String subject = objectMapper.readValue(object.getRequest(), ChangePasswordRequest.class).getUsername();
                            boolean isHelpdeskAgent = authentication.getAuthorities().anyMatch(authority -> authority.equals(HELPDESK_AGENT));
                            return new AuthorizationDecision(authentication.getPrincipal().getUsername().equals(subject) || isHelpdeskAgent);
                        }).build();
}

Теперь, очевидно, проблема довольно ясна, мне нужно разобрать тело запроса только для того, чтобы принять решение. Есть ли другой способ реализовать это? Может быть, иметь аутентификацию в части веб-безопасности, одновременно включая безопасность методов на контроллере и используя preauthorize? Я никогда не использовал безопасность методов раньше, так что это может быть невозможно, я не уверен.

Вот пример контроллера:

@RestController
@RequestMapping("/management")
public class ManagementController {

    @PutMapping("changePassword")
    public ResponseEntity<ChangePasswordResponse> changePassword(final ChangePasswordRequest changePasswordRequest) {
    }

}
  1. аутентифицировать пользователя, который отправил запрос
  2. если не удалось, вернуть 401
  3. если успешно, проверить, пытается ли аутентифицированный пользователь изменить свой собственный пароль, или он является агентом службы поддержки; в противном случае вернуть 403

Кажется довольно просто, но это вызывает у меня трудности в правильной реализации. Вышеупомянутое (разобрать тело запроса в authorizationmanager) работает, но кажется довольно подозрительным.

Ответ или решение

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

Шаг 1: включение метод-безопасности

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

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // Конфигурация безопасности будет здесь
}

Шаг 2: создание методовой безопасности

Далее, вы можете использовать аннотацию @PreAuthorize в вашем контроллере для указания логики авторизации, которая будет автоматически проверяться перед выполнением метода. Вам следует внедрить Authentication через аргумент метода и использовать @RequestBody для получения объекта ChangePasswordRequest.

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/management")
public class ManagementController {

    @PutMapping("changePassword")
    @PreAuthorize("isAuthenticated() && (" +
            "@securityService.canChangeOwnPassword(authentication, #changePasswordRequest.username) || " +
            "@securityService.isHelpdeskAgent(authentication))")
    public ResponseEntity<ChangePasswordResponse> changePassword(@RequestBody ChangePasswordRequest changePasswordRequest) {
        // Логика смены пароля
        return ResponseEntity.ok(new ChangePasswordResponse("Пароль успешно изменён"));
    }
}

Шаг 3: создание сервиса безопасности

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

import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

@Service
public class SecurityService {

    public boolean canChangeOwnPassword(Authentication authentication, String username) {
        return authentication.getName().equals(username);
    }

    public boolean isHelpdeskAgent(Authentication authentication) {
        return authentication.getAuthorities().stream()
                .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_HELPDESK_AGENT"));
    }
}

Как это работает?

  1. Когда поступает запрос на изменение пароля, Spring Security вызывает метод changePassword.
  2. Перед выполнением метода будет выполнен предикат, который проверяет, аутентифицирован ли пользователь и соответствует ли он необходимым условиям.
  3. Если предикат возвращает true, метод выполняется, в противном случае возвращается код 403 (Forbidden).

Заключение

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

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

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

Капча загружается...