Невозможно определить csrf

Невозможно определить csrf

Большинство разработчиков имеет только примерное представление о том что происходит внутри Spring Security, что опасно и может привести к появлению уязвимостей.

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

image

Подготовка проекта

Для начала подготовим проект, зайдем на https://start.spring.io/, поставим галочки напротив Web > web, и Core > Security.

Добавим контролер:

@RestController public class Controller {    @GetMapping   public String get() {     return String.valueOf(System.currentTimeMillis());   } }

Добавим rest-assured:

testCompile('io.rest-assured:rest-assured:3.0.2')

Добавим груви:

apply plugin: 'groovy'

Напишем тест:

ControllerIT.groovy

@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = "security.user.password=pass") class ControllerIT {    @LocalServerPort   private int serverPort;    @Before   void initRestAssured() {     RestAssured.port = serverPort;     RestAssured.filters(new ResponseLoggingFilter());     RestAssured.filters(new RequestLoggingFilter());   }    @Test   void 'api call without authentication must fail'() {     when()       .get("/")     .then()       .statusCode(HttpStatus.SC_UNAUTHORIZED);   } }

Запустим тест. Что в логах?

Запрос:

Request method:  GET Request URI:  http://localhost:51213/

Ответ:

HTTP/1.1 401  X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Strict-Transport-Security: max-age=31536000 ; includeSubDomains WWW-Authenticate: Basic realm="Spring" Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Sun, 22 Oct 2017 11:53:00 GMT  {   "timestamp": 1508673180745,   "status": 401,   "error": "Unauthorized",   "message": "Full authentication is required to access this resource",   "path": "/" }

SS без дополнительных настроек уже начал защищать вызовы методов, так как заработала конфигурация — SpringBootWebSecurityConfiguration поставляемая spring boot-ом. Внутри этого класса лежит ApplicationNoWebSecurityConfigurerAdapter который устанавливает дефолты.

На некоторые из них можно повлиять через настройки:
docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
искать «# SECURITY PROPERTIES», также можно заглянуть в код: SecurityProperties

Донастроим spring boot конфигурацию для полноты истории:

@TestPropertySource(properties = [     "security.user.password=pass",     "security.enable-csrf=true",     "security.sessions=if_required" ])

Фильтры

Работа Spring Security в веб приложении начинается с servlet фильтра.
Попробуем подебажить, но для начала добавим тест с успешной авторизацией.

@Test void 'api call with authentication must succeed'() {   given()     .auth().preemptive().basic("user", "pass")   .when()     .get("/")   .then()     .statusCode(HttpStatus.SC_OK); }

Поставим бряк и запустим тест.


рис. 1 — get метод

спустимся по огромному стеку вызовов (new Exception().getStackTrace().length == 91) и найдем первое упоминание спринга


рис. 2 — стэк вызовов

Посмотрим что лежит в переменной filterChain


рис. 3 — application filter chain

Здесь интересен фильтр springSecurityFilterChain именно он делает всю работу SS в веб части.

Сам DelegatingFilterProxyRegistrationBean не очень интересен, посмотрим кому он делегирует свою работу


рис. 4 — filter chain proxy

Свою работу он делегирует классу FilterChainProxy. Внутри него происходит несколько интересных вещей.

Прежде всего посмотрим на метод FilterChainProxy#doFilterInternal. Что здесь происходит? Получаем фильтры, создаем VirtualFilterChain и запускаем по ним запрос и ответ.

List<Filter> filters = getFilters(fwRequest); ... VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); 

Внутри метода getFilters берем первый SecurityFilterChain который совпадет с запросом.

private List<Filter> getFilters(HttpServletRequest request) {   for (SecurityFilterChain chain : filterChains) {     if (chain.matches(request)) {       return chain.getFilters();     }   }   return null; }

Зайдем в деббагер и посмотрим какой список итерируется.


рис. 5 — security filter chains

Что нам говорит этот список?

В обоих листах лежит OrRequestMatcher, который попытается сматчить текущий url с хотя бы с одним паттерном из списка.

Первый элемет списка имеет пустой список фильтров, соответсвенно никакой дополнительной фильтрации не будет, и как следствие не будет и защиты.

Проверим на практике.

Любой url который совпадет с этим паттернами, по умолчанию не будет защищен SS.
"/css/**", "/js/**", "/images/**", "/webjars/**", "/**/favicon.ico", "/error"

Добавим метод:

 @GetMapping("css/hello") public String cssHello() {   return "Hello I'm secret data"; } 

Напишем тест:

 @Test void 'get css/hello must succeed'() {   when()     .get("css/hello")   .then()     .statusCode(HttpStatus.SC_OK); } 

Гораздо интереснее второй SecurityFilterChain который совпадет с любым url «/**»

В нашем случае имеется следующий список фильтров.

0 = {WebAsyncManagerIntegrationFilter}  1 = {SecurityContextPersistenceFilter}  2 = {HeaderWriterFilter}  3 = {CsrfFilter}  4 = {LogoutFilter}  5 = {BasicAuthenticationFilter}  6 = {RequestCacheAwareFilter}  7 = {SecurityContextHolderAwareRequestFilter}  8 = {AnonymousAuthenticationFilter}  9 = {SessionManagementFilter}  10 = {ExceptionTranslationFilter}  11 = {FilterSecurityInterceptor} 

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

 http   .authorizeRequests().anyRequest().authenticated() .and()   .formLogin() .and()   .httpBasic(); 	

В этот список добавились бы фильтры:

UsernamePasswordAuthenticationFilter DefaultLoginPageGeneratingFilter 

В каком порядке фильтры идут по дефолту можно посмотреть здесь: FilterComparator

0 = {WebAsyncManagerIntegrationFilter}

Нам не очень интересен, согласно документации он «интегрирует» SecurityContext с WebAsyncManager который отвественнен за асинхронные запросы.

1 = {SecurityContextPersistenceFilter}

Ищет SecurityContext в сессии и заполняет SecurityContextHolder если находит.
По умолчанию используется ThreadLocalSecurityContextHolderStrategy которая хранит SecurityContext в ThreadLocal переменной.

2 = {HeaderWriterFilter}

Просто добавляет заголовки в response.

Отключаем кэш:

– Cache-Control: no-cache, no-store, max-age=0, must-revalidate
– Pragma: no-cache
– Expires: 0

Не разрешаем браузерам автоматически определять тип контента:

– X-Content-Type-Options: nosnif

Не разрешаем iframe

– X-Frame-Options: DENY

Включаем встроенную зашиту в браузер от cross-site scripting (XSS)

– X-XSS-Protection: 1; mode=block

3 = {CsrfFilter}

Пожалуй нет ни одного разработчика который при знакомстве с SS не столкнулся бы с ошибкой «отсутсвия csrf токена».

Почему мы не встречали эту ошибку ранее? Все просто, мы запускали методы на которых нет csrf защиты.

Попробуем добавить POST метод

   @PostMapping("post")   public String testPost() {     return "Hello it is post request";   } 

Тест:

 @Test void 'POST without CSRF token must return 403'() {   given()     .auth().preemptive().basic("user", "pass")   .when()     .post("/post")   .then()     .statusCode(HttpStatus.SC_FORBIDDEN); } 

Тест выполнился успешно, нам вернули 403 ошибку, csrf защита на месте.

4 = {LogoutFilter}

Далее идет logout фильтр, он проверяет совпадает ли url c паттерном
Ant [pattern='/logout', POST] - по умолчанию
и запускает процедуру логаута

handler = {CompositeLogoutHandler}   logoutHandlers = {ArrayList}  size = 2   0 = {CsrfLogoutHandler}    1 = {SecurityContextLogoutHandler} 

по дефолту происходит следующие:

  1. Удаляется Csrf токен.
  2. Завершается сессия
  3. Чистится SecurityContextHolder

5 = {BasicAuthenticationFilter}

Теперь мы добрались непосредственно до аутентификации. Что происходит внутри?
Фильтр проверяет, есть ли заголовок Authorization со значением начинающийся на Basic
Если находит, извлекает логин\пароль и передает их в AuthenticationManager

Внутри примерно такой код:

 if (headers.get("Authorization").startsWith("Basic")) {   try {   UsernamePasswordAuthenticationToken token = extract(header);   Authentication authResult = authenticationManager.authenticate(token);      } catch (AuthenticationException failed) {   SecurityContextHolder.clearContext();   this.authenticationEntryPoint.commence(request, response, failed);   return;   } } else {   chain.doFilter(request, response); } 

AuthenticationManager

 public interface AuthenticationManager {   Authentication authenticate(Authentication authentication)        throws AuthenticationException; } 

AuthenticationManager представляет из себя интрефейс, который принимает Authentication и возвращает тоже Authentication.

В нашем случае в имплементацией Authentication будет UsernamePasswordAuthenticationToken.
Можно было бы реализовать AuthenticationManager самому, но смысла в этом мало, существует дефолтная реализация — ProviderManager.

ProviderManager авторизацию делегирует другому интерфейсу:

 public interface AuthenticationProvider {   Authentication authenticate(Authentication authentication)        throws AuthenticationException;   boolean supports(Class<?> authentication); } 

Когда мы передаем объект Authentication в ProviderManager, он перебирает существующие AuthenticationProvider-ры и проверяет суппортит ли
AuthenticationProvider эту имплементацию Authentication

 public boolean supports(Class<?> authentication) {   return (UsernamePasswordAuthenticationToken.class           .isAssignableFrom(authentication)); } 

В результате внутри AuthenticationProvider.authenticate мы уже можем скастить переданный Authentication в нужную реализацию без каст эксепшена.

Далее из конкретной реализации вытаскиваем креденшеналы.

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

Более подробно и с картинками процесс описан здесь:
https://spring.io/guides/topicals/spring-security-architecture/

Далее BasicAuthenticationFilter сохраняет полученный Authentication в SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authResult);
Процесс аутентификации на этом завершен.

Если выбросится AuthenticationException то будет сброшен SecurityContextHolder.clearContext(); контекст и вызовится AuthenticationEntryPoint.

 public interface AuthenticationEntryPoint {   void commence(HttpServletRequest request,                 HttpServletResponse response,                AuthenticationException authException)  throws IOException, ServletException; } 

Задачей AuthenticationEntryPoint явялется записать в ответ информацию о том что аутентификация не удалась.

В случае бейсик аутентификации это будет:

 response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\""); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); 

В результате браузер покажет окошко basic авторизации.

6 = {RequestCacheAwareFilter}

Для чего нужен этот фильтр? Представим сценарий:

1. Пользователь заходит на защишенный url.
2. Его перекидывает на страницу логина.
3. После успешной авторизации пользователя перекидывает на страницу которую он запрашивал в начале.

Именно для для восстановления оригинального запроса существует этот фильтр.
Внутри проверяется есть ли сохраненный запрос, если есть им подменяется текущий запрос.
Запрос сохраняется в сессии, на каком этапе он сохраняется будет написанно ниже.

Попробуем вопроизвести.

Добавим метод:

 @GetMapping("customHeader") public String customHeader(@RequestHeader("x-custom-header") String customHeader) {   return customHeader; } 

Добавим тест:

@Test void 'passed x-custom-header must be returned'() {   def sessionCookie = given()       .header("x-custom-header", "hello")     .when()       .get("customHeader")     .then()       .statusCode(HttpStatus.SC_UNAUTHORIZED)       .extract().cookie("JSESSIONID")    given()       .auth().basic("user", "pass")       .cookie("JSESSIONID", sessionCookie)   .when()       .get("customHeader")   .then()       .statusCode(HttpStatus.SC_OK)       .body(equalTo("hello")); }

Как видим во втором запросе нам вернулся заголовок который мы передали в первом запросе. Фильтр работает.

7 = {SecurityContextHolderAwareRequestFilter}

Оборачивает существущий запрос в SecurityContextHolderAwareRequestWrapper

 chain.doFilter(this.requestFactory.create((HttpServletRequest) req, (HttpServletResponse) res), res); 

Имплементация может отличаться в зависимости от servlet api версии servlet 2.5/3

8 = {AnonymousAuthenticationFilter}

Если к моменту выполнения этого фильтра SecurityContextHolder пуст, т.е. не произошло аутентификации фильтр заполняет объект SecurityContextHolder анонимной аутентификацией — AnonymousAuthenticationToken с ролью «ROLE_ANONYMOUS».

Это гарарантирует что в SecurityContextHolder будет объект, это позволяет не бояться NP, а также более гибко подходить к настройке доступа для неавторизованных пользователей.

9 = {SessionManagementFilter}

На это этапе производятся действия связанные с сессией.

Это может быть:

— смена идентификатора сессии
— ограничени количества одновременных сессий
— сохранение SecurityContext в securityContextRepository

В нашем случае происходит следующе:
SecurityContextRepository с дефолтной реализацией HttpSessionSecurityContextRepository сохраняет SecurityContext в сессию.
Вызывается sessionAuthenticationStrategy.onAuthentication

Внутри sessionAuthenticationStrategy лежит:

sessionAuthenticationStrategy = {CompositeSessionAuthenticationStrategy}  delegateStrategies   0 = {ChangeSessionIdAuthenticationStrategy}    1 = {CsrfAuthenticationStrategy} 

Происходят 2 вещи:

1. По умолчанию включенна защита от session fixation attack, т.е. после аутенцификации меняется id сессии.
2. Если был передан csrf токен, генерируется новый csrf токен

Попробуем проверить первый пункт:

 @Test void 'JSESSIONID must be changed after login'() {    def sessionCookie = when()       .get("/")   .then()       .statusCode(HttpStatus.SC_UNAUTHORIZED)       .extract().cookie("JSESSIONID")    def newCookie = given()       .auth().basic("user", "pass")       .cookie("JSESSIONID", sessionCookie)   .when()       .get("/")   .then()       .statusCode(HttpStatus.SC_OK)       .extract().cookie("JSESSIONID")    Assert.assertNotEquals(sessionCookie, newCookie) } 

10 = {ExceptionTranslationFilter}

К этому моменту SecurityContext должен содеражть анонимную, либо нормальную аутентификацию.

ExceptionTranslationFilter прокидывает запрос и ответ по filter chain и обрабатывает возможные ошибки авторизации.

SS различает 2 случая:

1. AuthenticationException
Вызывается sendStartAuthentication, внутри которого происходит следующиее:

SecurityContextHolder.getContext().setAuthentication(null); — отчищает SecurityContextHolder
requestCache.saveRequest(request, response); — сохраняет в requestCache текущий запрос, чтобы RequestCacheAwareFilter было что восстанавливать.
authenticationEntryPoint.commence(request, response, reason); — вызывает authenticationEntryPoint — который записывает в ответ сигнал о том что необходимо произвести аутентификацию (заголовки \ редирект)

2. AccessDeniedException

Тут опять возможны 2 случая:

 if (authenticationTrustResolver.isAnonymous(authentication) ||    authenticationTrustResolver.isRememberMe(authentication)) {   ... } else {  ... } 

1. Пользователь с анонимной аутентификацией, или с аутентификацией по rememberMe токену
вызывается sendStartAuthentication

2. Пользователь с полной, не анонимной аутентификацией вызывается:
accessDeniedHandler.handle(request, response, (AccessDeniedException) exception)
который по дефолту проставляет ответ forbidden 403

11 = {FilterSecurityInterceptor}

На последнем этапе происходит авторизация на основе url запроса.
FilterSecurityInterceptor наследуется от AbstractSecurityInterceptor и решает, имеет ли текущий пользователь доступ до текущего url.

Существует другая реализация MethodSecurityInterceptor который отвественнен за допуск до вызова метода, при использовании аннотаций @Secured\@PreAuthorize.

Внутри вызывается AccessDecisionManager

Есть несколько стратегий принятия решения о том давать ли допуск или нет, по умолчанию используется: AffirmativeBased

код внутри очень простой:

 for (AccessDecisionVoter voter : getDecisionVoters()) {   int result = voter.vote(authentication, object, configAttributes);   switch (result) {   case AccessDecisionVoter.ACCESS_GRANTED:     return;   case AccessDecisionVoter.ACCESS_DENIED:     deny++;     break;   default:     break;   } } if (deny > 0) {   throw new AccessDeniedException(); } checkAllowIfAllAbstainDecisions(); 

Иными словами если кто-то голосует за, пропускаем, если хоть 1 голосует против не пускаем, если никто не проголосовал не пускаем.

Подведем небольшой итог:

springSecurityFilterChain — набор фильтров spring security.

Пример набора фильтров для basic авторизации:

WebAsyncManagerIntegrationFilter — Интегрирует SecurityContext с WebAsyncManager
SecurityContextPersistenceFilter — Ищет SecurityContext в сессии и заполняет SecurityContextHolder если находит
HeaderWriterFilter — Добавляет «security» заголовки в ответ
CsrfFilter — Проверяет на наличие сsrf токена
LogoutFilter — Выполняет logout
BasicAuthenticationFilter — Производит basic аутентификацию
RequestCacheAwareFilter — Восстанавливает сохраненный до аутентификации запрос, если такой есть
SecurityContextHolderAwareRequestFilter — Оборачивает существущий запрос в SecurityContextHolderAwareRequestWrapper
AnonymousAuthenticationFilter — Заполняет SecurityContext ананонимной аутентификацией
SessionManagementFilter — Выполняет действия связанные с сессией
ExceptionTranslationFilter — Обрабатывает AuthenticationException\AccessDeniedException которые происходят ниже по стеку.
FilterSecurityInterceptor — Проверяет имеет ли текущей пользователь доступ к текущему url.

FilterComparator — здесь можно посмотреть список фильтров и их возможный порядок.

AuthenticationManager — интерфейс, ответственнен за аутентификацию
ProviderManager — реализация AuthenticationManager, которая использует внутри использует AuthenticationProvider
AuthenticationProvider — интерфейс, отвественнен за аутентификаци конкретной реализации Authentication.
SecurityContextHolder — хранит в себе аутентификацию обычно в ThreadLocal переменной.
AuthenticationEntryPoint — модифицирует ответ, чтобы дать понять клиенту что необходима аутентификация (заголовки, редирект на страницу логина, т.п.)

AccessDecisionManager решает имеет ли Authentication доступ к какому-то ресурсу.
AffirmativeBased — стратегия используемая AccessDecisionManager по умолчанию.

Рекомендации

Напишите простые тесты, которые протестируют порядок фильтров и их настройки

это позволит избежать неприятных неожиданностей.
FilterChainIT.groovy

 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class FilterChainIT {    @Autowired   FilterChainProxy filterChainProxy;    @Autowired   List<Filter> filters;    @Test   void 'test main filter chain'() {     assertEquals(5, filters.size());      assertEquals(OrderedCharacterEncodingFilter, filters[0].getClass())     assertEquals(OrderedHiddenHttpMethodFilter, filters[1].getClass())     assertEquals(OrderedHttpPutFormContentFilter, filters[2].getClass())     assertEquals(OrderedRequestContextFilter, filters[3].getClass())      assertEquals("springSecurityFilterChain", filters[4].filterName)   }    @Test   void 'test security filter chain order'() {     assertEquals(2, filterChainProxy.getFilterChains().size());      def chain = filterChainProxy.getFilterChains().get(1);      assertEquals(chain.filters.size(), 11)      assertEquals(WebAsyncManagerIntegrationFilter, chain.filters[0].getClass())     assertEquals(SecurityContextPersistenceFilter, chain.filters[1].getClass())   }    @Test   void 'test ignored patterns'() {     def chain = filterChainProxy.getFilterChains().get(0);      assertEquals("/css/**", chain.requestMatcher.requestMatchers[0].pattern);     assertEquals("/js/**", chain.requestMatcher.requestMatchers[1].pattern);     assertEquals("/images/**", chain.requestMatcher.requestMatchers[2].pattern);   } } 

Не вызывайте SecurityContextHolder.getContext().getAuthentication(); для получения текущего юзера

Authentication — сам по себе не очень удобный для использования объект. Почти все методы возвращают Object, а чтобы получить нужную информацию нужно кастить в конкретную реализацию.

Лучше заведите интерфейс, сделайте реализацию в зависимости от потребностей, напишите HandlerMethodArgumentResolver.

Код с таким подходом лучше читать, тестировать, поддерживать.

 interface Auth {   ... }  public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {    @Override   public boolean supportsParameter(MethodParameter parameter) {     return parameter.getParameterType().equals(Auth.class);   }    @Override   public Auth resolveArgument(MethodParameter parameter,                  ModelAndViewContainer mavContainer,                 NativeWebRequest webRequest,                  WebDataBinderFactory binderFactory) {      Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();      return toAuth(principal)   } }  @GetMapping public String get(Auth auth) {   return "hello " + auth.getId(); } 

Расширяйте существующие реализации

Spring security содержит множество интрефейсов которые можно имплементировать, но скорее всего существует абрактный класс который на 99% делает то что вам нужно.

Например для интерфейса Authentication, существует AbstractAuthenticationToken, а аутенфикационный фильтр разумно отнаследовать от AbstractAuthenticationProcessingFilter

Используйте SecurityConfigurerAdapter чтобы сконфигурировать вашу аутентификацию

В том случае если у вас полностью кастомная аутентификация, скорее всего вам пришлось сделать следующее:

1. Создать реализацию Authentication
2. Создать AuthenticationProvider который поддерживает вашу реализацию Authentication
3. Добавить фильтр который начинал процесс аутентификации.

Разумно объеденить их всех в одном месте. Посмотрите на HttpBasicConfigurer, OpenIDLoginConfigurer они делают тоже самое.

 class MyConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {      @Override   public void configure(HttpSecurity http) throws Exception {      AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);     MyAuthenticationProvider myAuthenticationProvider = http.getSharedObject(MyAuthenticationProvider.class);      MyAuthenticationFilter filter = new MyAuthenticationFilter(authenticationManager);          http       .authenticationProvider(myAuthenticationProvider)       .addFilterBefore(postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class);   } }  public class SecurityConfig extends WebSecurityConfigurerAdapter {    protected void configure(HttpSecurity http) throws Exception {     http       .authorizeRequests().anyRequest().authenticated()     .and()       .apply(new MyConfigurer())   } }    

Для ограничения вызова методов по ролям используйте @Secured\@PreAuthorize

Напишите тест который пройдет по всем методам конроллеров и проверит наличие @Secured\@PreAuthorize аннотаций.

При настройке WebSecurityConfigurerAdapter требуйте наличия авторизации для всех url. При необходимости добавляйте исключения. Исключения должны быть как можно более строгие.
Явно указывайте тип http метода, а url должен быть как можно более полным.

Лучше явно указать полный путь до метода, даже если на момент написания других api с таким endpoint-ом не было.

Например если есть контроллер с двумя GET методами: "url/methodOne", "url/methodTwo",
Не стоит делать так:

  authorizeRequests().antMatchers(HttpMethod.GET, "url/**").permitAll(). 

Лучше напишите:

  authorizeRequests().antMatchers(HttpMethod.GET, "url/methodOne", "url/methodTwo").permitAll(). 

В случае проблем включите org.springframework.security: debug

Spring Security имеет достаточно подробные debug логи, зачастую одних их достаточно чтобы понять суть проблемы.

Различайте antMatchers(«permit_all_url»).permitAll() и web.ignoring().antMatchers(«ignored_url»)

 @Override protected void configure(HttpSecurity http) throws Exception {   http.authorizeRequests().       anyRequest()       .authenticated()       .antMatchers("permit_all_url")       .permitAll(); }  @Override public void configure(WebSecurity web) throws Exception {   web.ignoring().antMatchers("ignored_url"); } 

В случае «ignored_url» будет проверяться на этапе выбора security filter chain и если url совпадет то будет использован пустой фильтр.

В случае «permit_all_url» проверка будет проходить на этапе AccessDecisionManager.

Ссылки

  1. https://github.com/VladDm93/spring-security-request-journey.git — код.
  2. https://spring.io/guides/topicals/spring-security-architecture — Обзор архитектуры Spring security.



Источник: habr.com


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