[Из песочницы] Путь запроса по внутренностям Spring Security
Большинство разработчиков имеет только примерное представление о том что происходит внутри Spring Security, что опасно и может привести к появлению уязвимостей.
В этой статье шаг за шагом пройдемся по пути http запроса, что поможет с пониманием настраивать и решать проблемы Spring Security.
Подготовка проекта
Для начала подготовим проект, зайдем на 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 filters = getFilters(fwRequest);
...
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
Внутри метода getFilters берем первый SecurityFilterChain
который совпадет с запросом.
private List 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}
по дефолту происходит следующие:
- Удаляется Csrf токен.
- Завершается сессия
- Чистится 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);
— отчищает SecurityContextHolderrequestCache.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 с WebAsyncManagerSecurityContextPersistenceFilter
— Ищет SecurityContext в сессии и заполняет SecurityContextHolder если находитHeaderWriterFilter
— Добавляет «security» заголовки в ответCsrfFilter
— Проверяет на наличие сsrf токенаLogoutFilter
— Выполняет logoutBasicAuthenticationFilter
— Производит basic аутентификациюRequestCacheAwareFilter
— Восстанавливает сохраненный до аутентификации запрос, если такой естьSecurityContextHolderAwareRequestFilter
— Оборачивает существущий запрос в SecurityContextHolderAwareRequestWrapperAnonymousAuthenticationFilter
— Заполняет 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 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 {
@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.