Пользовательский метод в @PreAuthorize

Я использую Java 17. Новейшая версия Spring Security 6.1.4.

Я пытаюсь создать собственный диспетчер авторизации, и думаю, мне удалось сделать это без особых хлопот.

@Component
public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {

    @Autowired
    private RoleRepository roleRepository;

    private boolean onlyBUCheck;
    private GrantedAuthority buToCheck = null;
    private GrantedAuthority authorityToCheck = null;

    public CustomAuthorizationManager(){
        System.out.println("created through here");
    };

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        //calling buCheck and authorityCheck
    }

    private AuthorizationDecision buCheck(Authentication authentication){
        //Logic
    }

    private AuthorizationDecision authorityCheck(Authentication authentication){
        //Logic
    }

    public void partOfBU(String bu){
        Assert.notNull(bu, "role cannot be null");
        this.onlyBUCheck = true;
        this.buToCheck = () -> bu;
    }

    public void hasAuthority(String bu, String authority) {
        Assert.notNull(authority, "role cannot be null");
        this.onlyBUCheck = false;
        this.buToCheck = () -> bu;
        this.authorityToCheck = () -> authority;
    }

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

    @PostMapping("/wtf")
    @PreAuthorize("customAuthorizationManager.partOfBU(bu.id())") //I get autocomplete here
    public ResponseEntity<Object> aaa(@P("bu") @RequestBody BusinessUnitDTO businessUnitDTO){
        System.out.println(businessUnitDTO);
        return new ResponseEntity<>(HttpStatus.OK);
    }

Поскольку у меня появляется автозаполнение на @PreAuthorize, я думаю, все настроено правильно. Но когда я запускаю его, метод partOfBU() не выполняется. Просто метод check() (который, конечно, не работает, потому что все не настроено).

Если я просто выполню @EnableMethodSecurity(prePostEnabled = true), я получу это исключение org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'customAuthorizationManager' cannot be found on object of type 'org.springframework.security.access.expression.method.MethodSecurityExpressionRoot' - maybe not public or not valid? Насколько я понимаю, это означает, что bean-компонент customAuthorizationManager куда-то не был внедрен, и поэтому я не могу использовать его в @PreAuthorize.

Итак, я нашел это в документах https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#custom-authorization-managers

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE) //Tried both with and without this. no idea what it does
    Advisor preAuthorize(CustomAuthorizationManager manager) {
        return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
    }
}

Это решило исключение, и теперь только partOfBU() не выполняется, хотя я его там вызываю. (Наверное, потому что я вколол его в нужное место)

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

    @PostMapping("/wtf")
    @PreAuthorize("customAuthorizationManager") //not calling the partOfBU() method
    public ResponseEntity<Object> aaa(@P("bu") @RequestBody BusinessUnitDTO businessUnitDTO){
        System.out.println(businessUnitDTO);
        return new ResponseEntity<>(HttpStatus.OK);
    }

Оказывается, @PreAuthorize не волнует, что там. Он просто вызывает attemptAuthorization() в AuthorizationManagerBeforeMethodInterceptor (где мы устанавливаем собственный AuthorizationManager)

private void attemptAuthorization(MethodInvocation mi) {
        this.logger.debug(LogMessage.of(() -> {
            return "Authorizing method invocation " + mi;
        }));
        AuthorizationDecision decision = this.authorizationManager.check(this.authentication, mi);
        this.eventPublisher.publishAuthorizationEvent(this.authentication, mi, decision);
        if (decision != null && !decision.isGranted()) {
            this.logger.debug(LogMessage.of(() -> {
                return "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and decision " + decision;
            }));
            throw new AccessDeniedException("Access Denied");
        } else {
            this.logger.debug(LogMessage.of(() -> {
                return "Authorized method invocation " + mi;
            }));
        }
    }

Я думаю, что моя ошибка заключается в том, что я перепутал, как должен выглядеть менеджер авторизации методов и тот, который используется в файлах конфигурации с помощью requestMatchers. Я реализовал свою, глядя на реализацию AuthorityAuthorizationManager вместо PreAuthorizeAuthorizationManager. Постараюсь приблизить реализацию к той, что в PreAuthorizeAuthorizationManager, и вернусь с результатами. (Просто нужно разобраться, как использовать SpEL и связанные с ним классы)

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
54
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Хорошо удалось заставить его работать. Я опубликую это здесь, если у кого-то возникнет такая же проблема в будущем. Возможно, это старый способ решения проблем (насколько я понял из весенней документации)

Теперь я последовал руководству dan1st, который предложил довольно внимательно вносить изменения самостоятельно.

Я сделал это с помощью кастома AuthorizationManager. Есть и другие способы заменить эту часть, но я сделал это так.

Сначала мы начнем с класса конфигурации. (если ты не знаешь, что это за гугл spring configuration class)

@Configuration
@EnableMethodSecurity(prePostEnabled = false) //1
class MethodSecurityConfig {

    @Bean
    public Advisor preAuthorize(CustomAuthorizationManager manager) { //2
        return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
    }

}
  1. Когда мы включаем безопасность метода, Spring имеет реализацию по умолчанию. И мы хотим его перезаписать. Поэтому мы отключаем его.
  2. Это метод, который «вводит» CustomAuthorizationManager в нужное место. ЭТО ТОЛЬКО PreAuthorize МЕТОД. Если вы хотите использовать другие, вам также придется перезаписать их методы. (Показано в старой документации) По какой-то причине в новых версиях они решили сделать пример меньше и, следовательно, его труднее понять.

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

методы: @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter и другие...

config: те, которые определены в классе конфигурации, например

@Configuration
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
          .authorizeHttpRequests(requests -> {
                    requests.anyRequest().hasAuthority("test"); //.hasAuthority() here is what I'm talking about
          }).build();
    }

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

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

У меня было два варианта авторизации моего приложения. Либо храните массу информации в SecurityContextHolder, либо запрашивайте базу данных для каждого запроса, требующего авторизации. Я выбрал 2-й вариант.

@Component("customAuthorizationManager")
public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {

    private final RoleService roleService; 
    //Injecting RoleService
    public CustomAuthorizationManager(RoleService roleService) {
        this.roleService = roleService;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {

        ExpressionParser parser = new SpelExpressionParser(); //1
        Expression expression = parser.parseExpression(invocation.getMethod().getAnnotation(PreAuthorize.class).value());//2

        CustomMethodSecurityExpressionHandler c = new CustomMethodSecurityExpressionHandler(roleService); //3
        EvaluationContext ec = c.createEvaluationContext(authentication,invocation); //4
        boolean granted = Boolean.TRUE.equals(expression.getValue(ec, Boolean.class)); //5

        return new AuthorizationDecision(granted);
    }
}

Теперь будет вызываться метод check(). (Даже если вы вызовете какие-либо другие методы в CustomAuthorizationManager в @PreAuthorize Spring, это не будет волновать и просто вызовет метод check())

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

  1. Я создаю синтаксический анализатор, который будет читать выражение SpEL, которое мы предоставили внутри @PreAuthorize.
  2. Затем с помощью этой сложной штуки (и только что созданного синтаксического анализатора) мы получаем выражение и сохраняем его в переменной.
  3. (3 и 4). Теперь нам нужно реализовать способ обработки выражения (реализации я покажу позже).
  4. (5) После этого мы используем реализации, которые мы создали в шагах 3 и 4, чтобы принять решение и вернуть true или false.

Я следовал вышеупомянутому руководству, предоставленному Дэном. В основном модифицируя то, что там было, под мои нужды.

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {

    private final RoleService roleService;
    //Injecting the RoleService again (manually this time as this isn't a component)
    //and cuz it's needed further down the line
    public CustomMethodSecurityExpressionHandler(RoleService roleService) {
        this.roleService = roleService;
    }

    @Override
    public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
        StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);

        MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue();

        CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(delegate.getAuthentication());
        root.setRoleService(roleService);
        context.setRootObject(root);

        return context;
    }

}

Теперь метод, который мы переопределяем, отличается от статьи. Я предполагаю, что это может работать с тем же, что используется в статье, но я не пробовал (поскольку эта реализация должна была быть для @EnableGlobalMethodSecurity, но в Spring Security 6 предлагается использовать @EnableMethodSecurity) Я сделал это именно так, потому что именно так это было предложено в последней весенней документации (если вы прочитаете об этом больше, я думаю, что не рекомендуется делать это этими двумя способами, но да)

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

Вы также можете увидеть последнюю вещь, которую нам нужно реализовать — CustomMethodSecurityExpressionRoot.

public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {

    private RoleService roleService;
    //No idea why these are needed. Just followed the guide
    private HttpServletRequest request;
    private Object filterObject;
    private Object returnObject;
    private Object target;

    public CustomMethodSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }
    
    //Here is where you add the methods you'll be using in @PreAuthorize together with their logic

    //Custom method you'll be using in @PreAuthorize
    public boolean partOfBU(Long buIdToCheck){
        //your logic
    }
    
    //Custom method you'll be using in @PreAuthorize
    public boolean authorityCheck(Long buIdToCheck, String authorityToCheck){
        //your logic
    }

    public void setRoleService(RoleService roleService){
        this.roleService = roleService;
    }

    @Override
    public void setFilterObject(Object filterObject) {
        this.filterObject = filterObject;
    }

    @Override
    public Object getFilterObject() {
        return this.filterObject;
    }

    @Override
    public void setReturnObject(Object returnObject) {
        this.returnObject = returnObject;
    }

    @Override
    public Object getReturnObject() {
        return this.returnObject;
    }

    @Override
    public Object getThis() {
        return target;
    }
}

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

И как теперь это назвать?

    @PostMapping("/wtf")
    @PreAuthorize("partOfBU(#bu.id())") //1
    public ResponseEntity<Object> aaa(@P("bu") @RequestBody BusinessUnitDTO businessUnitDTO){
        return new ResponseEntity<>(HttpStatus.OK);
    }
  1. Вызов метода с использованием значения параметра (описано в документации (вполне хорошо для разнообразия))

И это весь шабанг. Вероятно, мой способ устарел, но он, по крайней мере, работает.

Другие вопросы по теме