Перейти к основному содержимому

Распространение контекста безопасности Spring с помощью @Async

· 4 мин. чтения

1. Введение

В этом руководстве мы сосредоточимся на распространении принципала Spring Security с помощью @Async . ``

По умолчанию проверка подлинности Spring Security привязана к ThreadLocal , поэтому, когда поток выполнения запускается в новом потоке с @Async, это не будет аутентифицированным контекстом.

Это не идеально — давайте это исправим.

2. Зависимости Maven

Чтобы использовать асинхронную интеграцию в Spring Security, нам нужно включить следующий раздел в зависимости нашего pom.xml :

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.6.0</version>
</dependency>

Последнюю версию зависимостей Spring Security можно найти здесь .

3. Распространение безопасности Spring с помощью @Async

Давайте сначала напишем простой пример:

@RequestMapping(method = RequestMethod.GET, value = "/async")
@ResponseBody
public Object standardProcessing() throws Exception {
log.info("Outside the @Async logic - before the async call: "
+ SecurityContextHolder.getContext().getAuthentication().getPrincipal());

asyncService.asyncCall();

log.info("Inside the @Async logic - after the async call: "
+ SecurityContextHolder.getContext().getAuthentication().getPrincipal());

return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}

Мы хотим проверить, распространяется ли Spring SecurityContext на новый поток. Сначала мы регистрируем контекст перед асинхронным вызовом, затем запускаем асинхронный метод и, наконец, снова регистрируем контекст. Метод asyncCall() имеет следующую реализацию:

@Async
@Override
public void asyncCall() {
log.info("Inside the @Async logic: "
+ SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}

Как мы видим, только одна строка кода выводит контекст внутри нового потока асинхронного метода.

4. Конфигурация по умолчанию

По умолчанию контекст безопасности внутри метода @Async будет иметь нулевое значение.

В частности, если мы запустим асинхронную логику, мы сможем зарегистрировать объект Authentication в основной программе, но когда мы зарегистрируем его внутри @Async , он будет нулевым . Это пример вывода журнала:

web - 2016-12-30 22:41:58,916 [http-nio-8081-exec-3] INFO
o.foreach.web.service.AsyncService -
Outside the @Async logic - before the async call:
org.springframework.security.core.userdetails.User@76507e51:
Username: temporary; ...

web - 2016-12-30 22:41:58,921 [http-nio-8081-exec-3] INFO
o.foreach.web.service.AsyncService -
Inside the @Async logic - after the async call:
org.springframework.security.core.userdetails.User@76507e51:
Username: temporary; ...

web - 2016-12-30 22:41:58,926 [SimpleAsyncTaskExecutor-1] ERROR
o.s.a.i.SimpleAsyncUncaughtExceptionHandler -
Unexpected error occurred invoking async method
'public void com.foreach.web.service.AsyncServiceImpl.asyncCall()'.
java.lang.NullPointerException: null

Итак, как вы можете видеть, внутри потока-исполнителя наш вызов завершается ошибкой с NPE, как и ожидалось, потому что Принципал там недоступен.

5. Конфигурация асинхронного контекста безопасности

Если мы хотим иметь доступ к принципалу внутри асинхронного потока, так же как у нас есть доступ к нему снаружи, нам нужно создать bean- компонент DelegatingSecurityContextAsyncTaskExecutor :

@Bean 
public DelegatingSecurityContextAsyncTaskExecutor taskExecutor(ThreadPoolTaskExecutor delegate) {
return new DelegatingSecurityContextAsyncTaskExecutor(delegate);
}

Таким образом, Spring будет использовать текущий SecurityContext внутри каждого вызова @Async .

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

web - 2016-12-30 22:45:18,013 [http-nio-8081-exec-3] INFO
o.foreach.web.service.AsyncService -
Outside the @Async logic - before the async call:
org.springframework.security.core.userdetails.User@76507e51:
Username: temporary; ...

web - 2016-12-30 22:45:18,018 [http-nio-8081-exec-3] INFO
o.foreach.web.service.AsyncService -
Inside the @Async logic - after the async call:
org.springframework.security.core.userdetails.User@76507e51:
Username: temporary; ...

web - 2016-12-30 22:45:18,019 [SimpleAsyncTaskExecutor-1] INFO
o.foreach.web.service.AsyncService -
Inside the @Async logic:
org.springframework.security.core.userdetails.User@76507e51:
Username: temporary; ...

И вот мы здесь — как мы и ожидали, мы видим того же принципала внутри потока асинхронного исполнителя. **

**

6. Варианты использования

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

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

7. Заключение

В этом кратком руководстве мы представили поддержку Spring для отправки асинхронных запросов с распространяемым SecurityContext. С точки зрения модели программирования новые возможности кажутся обманчиво простыми.

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

Этот пример также доступен в виде проекта Maven на Github .