Статьи

Кеширование в Spring Boot с Spring Security

В этом посте я хотел бы поделиться уроком, извлеченным одной из команд в O & B. Они использовали Spring Boot с Spring Security.

По умолчанию все, что защищено Spring Security, отправляется в браузер со следующим заголовком HTTP:

1
Cache-Control: no-cache, no-store, max-age=0, must-revalidate

По сути, ответ никогда не будет кэшироваться браузером. Хотя это может показаться неэффективным, на самом деле есть веская причина для такого поведения по умолчанию. Когда один пользователь выходит из системы, мы не хотим, чтобы следующий зарегистрированный пользователь мог видеть ресурсы предыдущего пользователя (и это возможно, если они кэшированы).

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

Хорошо, что очень легко включить кэширование статического содержимого в Spring Boot (даже с Spring Security). Просто настройте период кеширования. Вот и все!

1
2
3
4
5
# Boot 2.x
spring.resources.cache.cachecontrol.max-age=14400
 
# Boot 1.x
spring.resources.cache-period=14400

Но есть некоторые ошибки! С некоторыми версиями это не так просто! Позвольте мне объяснить дальше.

Существует несколько способов возврата контента:

  1. Статическое содержимое с помощью автоматически настраиваемого обработчика статических ресурсов Spring Boot
  2. Метод контроллера, возвращающий имя представления (например, преобразуется в JSP)
  3. Метод контроллера, возвращающий HttpEntity (или ResponseEntity )

Включить кэширование статического содержимого

Первый (обслуживающий статический контент) обрабатывается путем настройки указанного свойства (обычно в application.properties как показано выше).

Установить через HttpServletResponse

Во втором случае метод обработчика контроллера может выбрать установку заголовков «Cache-Control» через параметр метода HttpServletResponse .

1
2
3
4
5
6
7
8
@Controller
... class ... {
    @RequestMapping(...)
    public String ...(..., HttpServletResponse response) {
        response.setHeader("Cache-Control", "max-age=14400");
        return ...; // view name
    }
}

Это работает, пока Spring Security не перезаписывает его.

Установить через HttpEntity / ResponseEntity

В третьем случае метод обработчика контроллера может выбрать установку заголовков «Cache-Control» возвращаемого объекта HTTP.

1
2
3
4
5
6
7
@Controller
... class ... {
    @RequestMapping(...)
    public ResponseEntity<...> ...(...) {
        return ResponseEntity.ok().cacheControl(...).body(...);
    }
}

Это работает, пока Spring Security еще не написал свои собственные заголовки «Cache-Control».

Под капотом

Под капотом

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

В Spring Security Web 4.0.x с 4.2.0 до 4.2.4 и выше происходит следующая последовательность действий:

  1. HeaderWriterFilter делегирует CacheControlHeadersWriter для записи заголовков «Cache-Control» (включая «Pragma» и «Expires»), если заголовки кэша не существуют.
  2. Метод обработчика контроллера (если соответствует) вызывается. Метод может:
    • Явно установите заголовок в HttpServletResponse .
    • Или установите заголовок в возвращенном HttpEntity или ResponseEntity (обратитесь к handleReturnValue() объекта HttpEntityMethodProcessor ).
      • Обратите внимание, что HttpEntityMethodProcessor записывает заголовки (из HttpEntity ) в фактический ответ, только если они еще не существуют . Это становится проблемой, поскольку еще в # 1 заголовки уже были установлены.
  3. Если ни один контроллер не обрабатывает запрос, то автоматически настраивается обработчик статических ресурсов Spring Boot. Он пытается обслуживать статический контент и, если настроен для кэширования, он перезаписывает заголовки «Cache-Control» (и очищает значения заголовков «Pragma» и «Expires», если они есть). Статический обработчик ресурса — это объект ResourceHttpRequestHandler (см. applyCacheControl() в его базовом классе WebContentGenerator ).
    • Однако в Spring Web MVC 4.2.5 WebContentGenerator записывает заголовки «Cache-Control» только в том случае, если он не существует! , Это становится проблемой, поскольку еще в # 1 заголовки уже были установлены.
    • В Spring Web MVC 4.2.6 и выше он добавляет заголовки «Cache-Control», даже если он уже существует. Таким образом, нет проблем, даже если заголовки были установлены в # 1.

В Spring Security Web 4.1.x, 4.2.5 и выше (в Spring Boot 1.5.11 используется версия 4.2.5) последовательность изменилась. Это выглядит примерно так:

  1. Метод обработчика контроллера (если соответствует) вызывается. Метод может:
    • Явно установите заголовок в HttpServletResponse .
    • Или установите заголовок в возвращенном HttpEntity или ResponseEntity (обратитесь к handleReturnValue() объекта HttpEntityMethodProcessor ).
      • Обратите внимание, что HttpEntityMethodProcessor записывает заголовки (из HttpEntity ) в фактический ответ, только если они еще не существуют . Нет проблем, поскольку заголовки еще не установлены.
  2. Если ни один контроллер не обрабатывает запрос, то автоматически настраивается обработчик статических ресурсов Spring Boot. Он пытается обслуживать статический контент и, если настроен для кэширования, он перезаписывает заголовки «Cache-Control» (и очищает значения заголовков «Pragma» и «Expires», если они есть).
  3. HeaderWriterFilter делегирует CacheControlHeadersWriter для записи заголовков «Cache-Control» (включая «Pragma» и «Expires»), если заголовки кэша не существуют.
    • Нет проблем, поскольку он не будет перезаписан, если заголовки кэша уже установлены.

Рабочие версии

Выше три случая управления кэшированием работают в Spring Boot 1.5.11 и Spring Boot 2.x. Но в случае, если обновление до этих версий невозможно, просмотрите следующие классы и проверьте, имеет ли он желаемое поведение (используя приведенные выше последовательности):

      • HeaderWriterFilter (см. Метод doFilterInternal )
      • CacheControlHeadersWriter (см. writeHeaders() )
      • WebContentGenerator (см. applyCacheControl() )
      • HttpEntityMethodProcessor (см. handleReturnValue() )

Также имейте в виду, что Spring Security Web 4.2.5 и выше запишет следующие HTTP-заголовки (перезаписывайте их, даже если они уже установлены, как, например, в контроллере):

      • X-Content-Type-Options через XContentTypeOptionsHeaderWriter
      • Strict-Transport-Security через HstsHeaderWriter
      • X-Frame-Options через XFrameOptionsHeaderWriter
      • X-XSS-Protection помощью XXssProtectionHeaderWriter

Это связано с тем, что, в отличие от CacheControlHeadersWriter , CacheControlHeadersWriter заголовков для вышеупомянутого не проверяют, существуют ли уже заголовки. Они просто устанавливают свои соответствующие заголовки HTTP. Пожалуйста, обратитесь к их соответствующим классам писателя заголовка и выпуск № 5193 .

Другой вариант — заставить Spring Security игнорировать статические запросы ресурсов. Таким образом, настроенный период кэширования не будет перезаписан.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**");
        // If the above paths are served by the
        // Spring Boot auto-configured
        // static resource request handler,
        // and a cache period is specified,
        // then it will have a "Cache-Control"
        // HTTP header in its response.
        // And it would NOT get overwritten by Spring Security.
    }
}

Это все на данный момент. Надеюсь, это прояснит ситуацию.

Смотреть оригинальную статью здесь: Кэширование в Spring Boot с Spring Security

Мнения, высказанные участниками Java Code Geeks, являются их собственными.