Вопрос или проблема
Ищу руководство, каким образом правильно поступить. У меня есть 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) {
}
}
- аутентифицировать пользователя, который отправил запрос
- если не удалось, вернуть 401
- если успешно, проверить, пытается ли аутентифицированный пользователь изменить свой собственный пароль, или он является агентом службы поддержки; в противном случае вернуть 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"));
}
}
Как это работает?
- Когда поступает запрос на изменение пароля, Spring Security вызывает метод
changePassword
. - Перед выполнением метода будет выполнен предикат, который проверяет, аутентифицирован ли пользователь и соответствует ли он необходимым условиям.
- Если предикат возвращает
true
, метод выполняется, в противном случае возвращается код 403 (Forbidden).
Заключение
Такой подход обеспечивает более чистую архитектуру и упрощает тестирование логики авторизации. Общие предикаты могут быть легко изменены или дополнены, при этом не требуется модифицировать логику контроллера. Важно отметить, что безопасность приложения требует тщательного обдумывания, и использование метод-безопасности может значительно улучшить кодовую базу, упрощая логику обработки прав доступа.
Если у вас остались вопросы или необходима дополнительная помощь по внедрению данной логики, не стесняйтесь задавать их!