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 .