Весна: Как инициализировать сервисный бин с информацией об аутентифицированном пользователе

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

Это кажется обычным случаем, но я всё ещё не могу найти подходящее решение.

У меня есть сервис, который сильно связан с текущим аутентифицированным пользователем. Он должен получать userId из более высокого контекста (Контроллер) так или иначе. Сервис не может получить userId из любого доступного глобально контекста ‘auth’, он должен быть получен в качестве аргумента.

Сервис также должен быть бином (@Service), поэтому он должен автоматически инициализироваться Spring.

Один из способов достижения этого следующий:

  1. Инициализировать сервис без userId
  2. Затем передавать userId в каждый метод, который вызывается на сервисе:
@Service
class MyService
  public void method1(int userId) {
    // что-то с userId
  }

  public void method2(int userId) {
    // что-то с userId
  }
end

// Использование
myService = new MyService(); // Это делает Spring (без проблем).
myService.method1(getUserIdFromAnyAuthContext())
myService.method2(getUserIdFromAnyAuthContext())

Вот как я бы хотел, чтобы мой сервис работал (если бы это не был бин):

@Service
class MyService
  private int currentUserId;

  public MyService(int currentUserId) {
    this.currentUserId = currentUserId;
  }

  public void method1() {
    // что-то с currentUserId
  }

  public void method2() {
    // что-то с currentUserId
  }
end

// Использование
myService = new MyService(getUserIdFromAnyAuthContext()); // Я хочу, чтобы Spring мог это сделать и сделать MyService бин.
myService.method1()
myService.method2()

Приведённый выше пример показывает, как бы я сделал это, если бы это не был бин. Суть в том, чтобы не передавать userId в каждый метод. Он должен быть инъецирован в сервис каким-то образом с каждым запросом, но в то же время это должно выглядеть так, как будто я передал его стандартным способом Java, чтобы его можно было тестировать независимо от внешнего контекста.

Как сделать сервис бином и инициализировать его данными ‘auth’?

Обновлено:

В случае, если это имеет значение (в ответ на комментарий @Anish B.): я собираюсь использовать Spring Security в качестве провайдера аутентификации. Но, пожалуйста, обратите внимание, что сам сервис не может получать доступ к любому глобально доступному контексту ‘auth’, он должен быть независим от внешнего контекста.

Spring Security создает бин Authentication, который содержит информацию о текущем пользователе. Если вы хотите избавиться от зависимости от Spring Security, просто используйте интерфейс Principal, который является родителем Authentication.

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

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

Концепция

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

Решение

  1. Создание пользовательского контекста:
    Создадим интерфейс UserContext, который будет хранить информацию о текущем пользователе и передавать ее в сервис.

    public interface UserContext {
       int getCurrentUserId();
    }
  2. Реализация UserContext:
    Реализуем этот интерфейс, чтобы получать данные из Spring Security.

    @Component
    public class SecurityUserContext implements UserContext {
    
       @Override
       public int getCurrentUserId() {
           // Получение идентификатора пользователя из Spring Security
           Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
           if (authentication != null && authentication.isAuthenticated()) {
               return ((CustomUserDetails) authentication.getPrincipal()).getUserId();
           }
           throw new IllegalStateException("Пользователь не аутентифицирован");
       }
    }
  3. Создание сервиса:
    Теперь можем создать сервис, который будет принимать в конструкторе UserContext.

    @Service
    public class MyService {
    
       private final int currentUserId;
    
       public MyService(UserContext userContext) {
           this.currentUserId = userContext.getCurrentUserId();
       }
    
       public void method1() {
           // Логика с использованием currentUserId
       }
    
       public void method2() {
           // Логика с использованием currentUserId
       }
    }
  4. Обработка контекста пользователя в контроллере:
    Контроллер будет получать экземпляр UserContext и использовать его для инициализации сервиса. Это может быть также сделано с помощью внедрения зависимостей.

    @RestController
    public class UserController {
    
       private final UserContext userContext;
       private final MyService myService;
    
       public UserController(UserContext userContext, MyService myService) {
           this.userContext = userContext;
           this.myService = myService;
       }
    
       @GetMapping("/some-endpoint")
       public ResponseEntity<?> someMethod() {
           myService.method1();
           return ResponseEntity.ok("Успех");
       }
    }

Преимущества предложенного подхода

  • Отделение от глобального контекста: Сервис не зависит от статических методов или глобальных переменных, он полностью изолирован и может быть протестирован независимо от состояния аутентификации.

  • Легкость тестирования: Можно легко создавать мок-объекты UserContext для написания юнит-тестов, что упрощает процесс тестирования.

  • Поддержка Spring: Использование Spring для управления жизненным циклом бинов и инъекций.

Заключение

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

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

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