У меня есть проект с шлюзом Spring в качестве клиента oauth для сервера авторизации Spring. С точки зрения аутентификации oidc все работает нормально, кроме выхода из системы. Выход из системы не работает из-за CORS, так как каким-то образом после нажатия кнопки выхода из системы на странице выхода Spring по умолчанию источник имеет значение null. Если я добавлю «нулевой» источник в список разрешенных источников или отключу cors, он будет работать как положено.
Ниже приведена конфигурация клиента Spring Gateway:
@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {
@Autowired
public SecurityConfiguration(ReactiveClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
ReactiveClientRegistrationRepository clientRegistrationRepository;
private ServerLogoutSuccessHandler serverLogoutSuccessHandler() {
OidcClientInitiatedServerLogoutSuccessHandler successHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
successHandler.setPostLogoutRedirectUri("{baseUrl}/welcome");
return successHandler;
}
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
String[] unprotectedPaths = new String[]{"/api-docs/**", "/swagger-ui.html", "/webjars/swagger-ui/**", "/actuator/**",
"/oidc/**","/welcome/**", "/customer/registration", "/customer/registration-confirmation/**"};
http.cors(withDefaults()).csrf(csrfSpec -> csrfSpec.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaServerCsrfTokenRequestHandler()))
.authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec.pathMatchers(unprotectedPaths).permitAll()
.pathMatchers("/notification/ws-connect").hasAuthority("SCOPE_notification.read")
.anyExchange().authenticated())
.oauth2Login(withDefaults())
.oauth2ResourceServer(oAuth2ResourceServerSpec -> oAuth2ResourceServerSpec.jwt(withDefaults()))
.logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer.logoutSuccessHandler(serverLogoutSuccessHandler()));
return http.build();
}
static final class SpaServerCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {
private final ServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();
@Override
public void handle(ServerWebExchange exchange, Mono<CsrfToken> csrfToken) {
// Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the CsrfToken when it is rendered in the response body.
this.delegate.handle(exchange, csrfToken);
}
@Override
public Mono<String> resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
final var hasHeader = exchange.getRequest().getHeaders().get(csrfToken.getHeaderName()) != null;
return hasHeader ? super.resolveCsrfTokenValue(exchange, csrfToken) : this.delegate.resolveCsrfTokenValue(exchange, csrfToken);
}
}
@Bean
public WebFilter csrfCookieWebFilter() {
return (exchange, chain) -> {
exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()).subscribe(o -> ((CsrfToken) o).getToken());
return chain.filter(exchange);
};
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:63342"));
configuration.setAllowedMethods(List.of(CorsConfiguration.ALL));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(List.of(CorsConfiguration.ALL));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Ниже приведен журнал API-шлюза:
2024-07-16T22:37:22.392+03:00 DEBUG 90681 --- [api-gateway] [ctor-http-nio-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=GET}
2024-07-16T22:37:22.392+03:00 DEBUG 90681 --- [api-gateway] [ctor-http-nio-6] athPatternParserServerWebExchangeMatcher : Checking match of request : '/logout'; against '/logout'
2024-07-16T22:37:22.392+03:00 DEBUG 90681 --- [api-gateway] [ctor-http-nio-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : matched
2024-07-16T22:37:22.395+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter : [130c2a73-11] Completed 200 OK, headers = {masked}
2024-07-16T22:37:22.395+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.h.s.r.ReactorHttpHandlerAdapter : [130c2a73-5, L:/[0:0:0:0:0:0:0:1]:9990 - R:/[0:0:0:0:0:0:0:1]:63469] Handling completed
2024-07-16T22:37:37.036+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter : [130c2a73-12] HTTP POST "/logout", headers = {masked}
2024-07-16T22:37:37.038+03:00 DEBUG 90681 --- [api-gateway] [ctor-http-nio-6] o.s.w.c.reactive.DefaultCorsProcessor : Reject: 'null' origin is not allowed
2024-07-16T22:37:37.038+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter : [130c2a73-12] Completed 403 FORBIDDEN, headers = {masked}
2024-07-16T22:37:37.039+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.h.s.r.ReactorHttpHandlerAdapter : [130c2a73-6, L:/[0:0:0:0:0:0:0:1]:9990 - R:/[0:0:0:0:0:0:0:1]:63469] Handling completed
Позже редактировать:
Кажется, Chrome рассматривает два разных порта на локальном хосте как один и тот же сайт. Кроме того, JSESSIONID, используемый Spring Security, не является SameSite, поэтому он будет отправлен в запросе из перекрестного источника. Однако моя первоначальная проблема не связана с запросами из перекрестного источника.
Кроме того, после того, как вы вызовете выход из системы в клиенте Spring с помощью OidcClientInitiatedServerLogoutSuccessHandler
, если выход из системы завершится успешно, ответом должно быть перенаправление. Что это за ответ (статус и местоположение)? Как вы следите за этим перенаправлением?
SPA и шлюз не будут иметь одинаковое происхождение, не знаю, почему вы так думаете. Пока шлюз настроен на разрешение источника SPA, они могут совместно использовать файл cookie сеанса. Также я использую oauth2ResourceServer
только для доступа к нему из Postman с помощью токена на предъявителя. Так не будет, поскольку нет смысла быть одновременно клиентом и сервером ресурсов. После нажатия кнопки выхода на странице /logout я получаю ошибку 403.
Я пробовал как /logout из api-gateway(localhost:9990/logout ), так и из auth-сервера( localhost:9989/logout), но ни один из них не вышел из системы и не перенаправился. При выходе из сервера аутентификации выводится сообщение о том, что вы вышли из системы, но перенаправления не происходит.
RP. Выход из системы, инициированный RP, означает «Доверяющая сторона». Это означает, что выход из системы начинается с клиента, который закрывает свой сеанс, и, если операция успешна (найден сеанс для закрытия), перенаправляет пользовательский агент на OP (он же провайдер OpenID или сервер авторизации), чтобы OP мог завершить свой собственный сеанс для вышедшего из системы пользователя. Поэтому выйдите из шлюза и предоставьте дополнительные журналы отладки и сведения о перенаправлении.
«SPA и шлюз не будут иметь одинаковое происхождение, не знаю, почему вы думаете, что они должны», как я уже писал, файл cookie сеанса должен быть помечен как тот же сайт. Заголовков CORS будет недостаточно, чтобы браузер мог поделиться этим файлом cookie Spring с другими источниками (службами, работающими на разных портах). Вам нужен обратный прокси-сервер, а не CORS. WebPack One отлично подходит для разработки. В любой среде вы можете использовать шлюз в качестве прокси (для маршрутизации к SPA). Вы также можете использовать экземпляр nginx или любой другой вход перед обоими. Последний вариант, который работает при упаковке SPA, — это обслуживание его из статических ресурсов шлюза.
Я использую шлюз в качестве прокси, и, насколько я понимаю, все должно быть в порядке. Все работает нормально, кроме выхода из системы. У меня есть другие нижестоящие службы, которые получают всю информацию, необходимую для работы. Также в качестве примечания: у вас может быть несколько клиентов и один шлюз. Клиенты не обязаны иметь одно и то же происхождение.
Почему при выходе из системы запрашиваются земли null
происхождения? Не попытаетесь ли вы отправить его с помощью Postman вместо использования уже авторизованного браузера (с активным сеансом на шлюзе)?
Вот это меня тоже смущает. Для этого теста я использую не Postman, а браузер. Единственное, что я делаю, это вхожу в систему, а затем перехожу к api-gateway/logout, где получаю страницу подтверждения выхода, на которой я выхожу из системы... вот и все.
аналогичная проблема опубликована здесь и, судя по всему, она будет работать нормально, если ее не развернуть на локальном компьютере. На данный момент я добавлю «null» в список разрешенных источников CORS. Я также обновлю, когда появится новая версия Spring Security, так как, похоже, это проблема Spring, а не конфигурация.
У вас по-прежнему будет одна и та же цепочка фильтров, настроенная как с отслеживанием состояния (oauth2Login
) и без сохранения состояния (oauth2ResourceServer
), с защитой от CSRF, необходимой для некоторых запросов и бесполезной для других, необходимостью использования разных стратегий сопоставления полномочий, необходимостью различного статуса ответа для несанкционированные запросы... А как насчет попыток понять OAuth2 и преимущества безопасности хранения токенов на сервере?
Как я уже упоминал, (oauth2ResourceServer)
предназначен для использования с почтальоном и является временным. Насколько мне известно по Oauth, это никак не влияет на выход из системы. Если вы считаете, что использование шлюза в качестве клиента и сервера ресурсов каким-либо образом повлияет на проблему, которую я описал в вопросе, объясните. Мой вопрос был очень конкретным. Я уже пробовал удалить конфигурацию сервера ресурсов, но это не решило проблему.
Я также подробно объяснил, чего не хватает в вашем вопросе, чтобы мы могли понять вашу проблему (точный запрос, используемый для инициирования выхода из системы: что его отправляет, HTTP-команд, URL-адрес, полезная нагрузка). Я также четко заявил о том, что oauth2Login
(и выход из системы) основаны на сеансе и что файл cookie сеанса должен быть помечен с тем же сайтом, что является причиной необходимости того же источника вместо CORS для запросов к вашему бэкэнду. Наконец, вам не нужен перекрестный запрос к конечной точке авторизации сервера авторизации. Установка windows.location.href
— это нормально.
Но поскольку вы, похоже, также размещаете сервер авторизации, вы можете разместить его за тем же обратным прокси-сервером, что и шлюз и ваш интерфейс, имея один и тот же источник для всего (больше никаких проблем с CORS, конфигурации CORS или взлома для отслеживания авторизации с помощью установка windows.location.href
в Javascript вместо того, чтобы позволить браузеру следовать за 302
. Это может даже позволить вам отображать формы входа в рамке внутри интерфейса, а не покидать его).
Последнему почтальону не нужно проходить через шлюз. Он может отправлять запросы непосредственно на сервер ресурсов. Если вы действительно хотите, чтобы он проходил через шлюз (и разрабатывался с конфигурацией безопасности, отличной от prod), определите вторую цепочку фильтров без oauth2Login
и второй набор маршрутов без TokenRelay
фильтра.
Извиняюсь, если я неправильно понял, но вы говорите об изменении моей архитектуры. Это не то, что я ищу. У меня будет SPA-клиент (в настоящее время веб-страница, работающая на HTTP-сервере Intelj), который может не быть развернут в том же домене, что и шлюз, поэтому необходимы заголовки CORS. Серверные службы работают нормально, поскольку им не нужен файл cookie сеанса, а только токен, который поступает через TokenRelay. Шлюз работает как прокси. Серверные службы будут скрыты от внешнего мира и доступны только через шлюз, отсюда и ситуация с почтальоном.
Я добавил пример на github.com/ionutbarau/demo-stackoverflow. Это было предназначено для другого вопроса, но может быть использовано для воспроизведения ошибки. Просто проигнорируйте первую часть README и выполните шаги 1–3 из раздела «Как использовать». Чтобы воспроизвести проблему, просто попробуйте выйти из системы (укажите в браузере localhost:9990/logout) после входа в систему. Страницы входа/выхода являются стандартными для Spring Security.
«Измени мою архитектуру. Это не то, что я ищу». изменить вашу архитектуру кажется проще, чем изменить способ защиты файлов cookie сеанса браузерами...
пожалуйста, попробуйте пример, который я опубликовал, и выполните точные шаги для воспроизведения. При воспроизведении не используется SPA (не требуется перекрестный запрос). Также опубликуйте раздел в документации, где говорится, что для того, чтобы выход из системы, инициированный RP, работал, и SPA, и клиент oauth должны находиться в одном домене. Файл cookie сеанса всегда будет отправляться при вызове домена, который его выдал, независимо от того, кто его отправляет (именно так происходят атаки CSRF). Поэтому вполне нормально иметь SPA в другом источнике, через который вы выполняете POST для /logout.
«Файл cookie сеанса всегда будет отправляться при вызове домена, который его выдал». Это неправильно. Прочтите о флаге SameSite
для файлов cookie. Сеансовые файлы cookie всегда должны быть помечены SameSite
(весенние файлы есть, проверьте инструменты отладки браузера). И я уже потратил достаточно времени здесь, пока.
Выход из системы, инициированный RP (откройте ссылку и прочтите спецификацию) начинается после того, как выход был выполнен на проверяющей стороне (приложение Spring, настроенное с помощью oauth2Login
). Чтобы инициировать этот первый выход из системы, SPA должен POST
подключиться к конечной точке шлюза /logout
.
Как и любой запрос к приложению, настроенному с помощью oauth2Login
, он авторизуется с помощью сеанса (запрос должен содержать файл cookie сеанса). По этой причине внешний интерфейс, отправляющий этот запрос, должен обслуживаться с того же хоста, что и RP.
В случае SPA на сегодняшний день самым простым вариантом является обслуживание SPA и BFF через один и тот же обратный прокси-сервер (который может быть самим шлюзом). Это удаляет запросы перекрестного происхождения между интерфейсом и сервером. Если SPA должен обслуживаться из домена, отличного от API, то BFF (шлюз с oauth2Login
и фильтром TokenRelay
) должен размещаться в этом другом домене.
Как и любой запрос POST
, PUT
, PATCH
или DELETE
, авторизованный с помощью сеанса, он должен быть защищен от CSRF (содержать токен CSRF). В случае одностраничного приложения этот токен считывается из файла cookie (с значением «только для http» — false) и устанавливается в качестве заголовка.
Ответ на запрос POST
(после завершения сеанса шлюза) должен содержать заголовок Location
с URI для завершения сеанса на сервере авторизации. Если источник, из которого обслуживается SPA (обратный прокси-сервер), разрешен на сервере авторизации, вы можете разрешить браузеру, в котором запускается SPA, следовать по этому местоположению. В противном случае вам, возможно, придется изменить статус ответа шлюза с 302
на что-то в диапазоне 2xx
, чтобы код Javascript мог наблюдать за ответом, читать Location
и затем устанавливать window.location.href
(изменить начало вкладки браузера вместо отправки запрос из перекрестного источника).
Подробный рабочий пример (включая выход из системы) spring-cloud-gateway
, используемого в качестве OAuth2 BFF (с oauth2Login
и фильтром TokenRelay
) для SPA в этой статье Baeldung, которую я написал.
Видимо, есть веская причина иметь «нулевой» Origin. Любой желающий может проверить этот ТАКОЙ вопрос для получения дополнительной информации.
По сути, если во время запроса CORS происходит перенаправление на другой сервер, заголовок Origin будет изменен на «нулевой», поскольку это контекст, чувствительный к конфиденциальности. Это происходит в моей ситуации, поскольку выполнение POST со страницы выхода Spring по умолчанию к действию шлюза/выхода делает его запросом CORS. Во время этого запроса происходит перенаправление на сервер авторизации, поэтому браузер устанавливает для заголовка Origin значение null. Поскольку у меня настроен CORS, запрос отклоняется. Сначала я запутался, подумав, что, поскольку я не использую SPA для выхода из системы, CORS здесь не задействован.
Кажется, то, чего я хотел достичь, уже не осуществимо.
Конечная цель состояла в том, чтобы развернуть SPA на mydomain:80 и шлюз на mydomain:9990 (настройка аналогична локальному хосту). Теоретически это должно работать, поскольку это один и тот же сайт, но разное происхождение. Это означает, что файл cookie сеанса будет отправлен, но для отправки запроса на публикацию необходимо настроить CORS. Я хотел сохранить простоту (без прокси-сервера перед SPA и шлюзом), но также не хотел объединять SPA в шлюзе.
В качестве решения мне придется переосмыслить свою настройку и попытаться обслуживать SPA через шлюз, как предложил @ch4mp.
Как упоминалось в комментариях, вы можете использовать шлюз в качестве обратного прокси-сервера для SPA: просто определите для него маршрут (без фильтра TokenRelay, который следует переместить из фильтров по умолчанию в настройку маршрутов к серверам ресурсов).
Да, я сделал это, и это сработало. Я также удалю фильтр TokenRelay, но, похоже, он с ним работает. Спасибо за помощь !
Да, работает, как только будет маршрут до СПА permitAll()
. Но. Вы можете сэкономить немного ресурсов: то, что обслуживает активы SPA, не требует токена доступа. Кроме того, если вы используете Actuator на шлюзе, вы можете захотеть защитить его конечные точки с помощью цепочки фильтров сервера ресурсов (отличной от клиентской с oauth2Login
, без сеансов и используемой клиентами на вашем бэкэнде, например, монитором кластера). Таким образом, вы можете использовать эту цепочку фильтров сервера ресурсов (или еще одну, вообще не защищенную) и сохранять ресурсы, используемые сеансами, и фильтр TokenRelay для запроса доступа к ресурсам SPA.
Почему вы добавляете конфигурацию CORS? Разве ваш SPA и шлюз не должны иметь одинаковое происхождение, чтобы совместно использовать файл cookie сеанса (требуется для работы
oauth2Login
иTokenRelay
фильтра)? Почему вы настраиваетеoauth2ResourceServer
в цепочке фильтров, уже настроенной с помощьюoauth2Login
? Требования безопасности для 2 сильно различаются: необходимость в сеансах и защите CSRF, авторизация на основе файлов cookie и токенов VS, статус ответа для неавторизованного доступа. Запросы,...