Spring с аутентификацией Oauth2 или Http-Basic для одного и того же ресурса
Я пытаюсь реализовать API с ресурсами, которые защищены аутентификацией Oauth2 ИЛИ Http-Basic.
Когда я загружаю WebSecurityConfigurerAdapter, который сначала применяет HTTP-базовую аутентификацию к ресурсу, аутентификация маркера Oauth2 не принимается. И наоборот.
Примеры конфигураций:
Это относится к HTTP-базовой аутентификации ко всем /user/ ** resources
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private LoginApi loginApi;
@Autowired
public void setLoginApi(LoginApi loginApi) {
this.loginApi = loginApi;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new PortalUserAuthenticationProvider(loginApi));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/users/**").authenticated()
.and()
.httpBasic();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Это относится к защите маркера oauth к ресурсу /user/ **
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/users/**")
.and()
.authorizeRequests()
.antMatchers("/users/**").access("#oauth2.clientHasRole('ROLE_CLIENT') and #oauth2.hasScope('read')");
}
}
Я уверен, что есть какой-то фрагмент волшебного кода, который мне не хватает, который сообщает spring попробовать оба, если первый не сработал?
Любая помощь будет наиболее оценена.
Ответы
Ответ 1
Мне удалось получить эту работу на основе намеков Майкла Ресслера, но с некоторыми настройками.
Моя цель состояла в том, чтобы позволить как Basic Auth, так и Oauth на тех же конечных точках ресурсов, например, /leafcase/ 123. Я был захвачен в течение довольно долгого времени из-за упорядочивания filterChains (можно проверить в FilterChainProxy.filterChains); порядок по умолчанию выглядит следующим образом:
- Сервер аутентификации Oauth (если включен в том же проекте) filterChains. по умолчанию 0 (см. AuthorizationServerSecurityConfiguration)
- Фильтр ресурсов ресурса OauthChains. по умолчанию 3 (см. ResourceServerConfiguration). Он имеет логику сопряжения запросов, которая соответствует чему-либо, кроме конечных точек аутентификации Oauth (например,/oauth/token,/oauth/authorize и т.д. См. ResourceServerConfiguration $NotOauthRequestMatcher.matches()).
- ФильтрChains, который соответствует config (HttpSecurity http) - по умолчанию 100, см. WebSecurityConfigurerAdapter.
Поскольку ресурсный сервер filterChains занимает более высокое значение, чем у настроенного фильтра WebSecurityConfigurerAdapter, и первый соответствует практически каждой конечной точке ресурса, тогда логика сервера Oauth всегда запускается для любого запроса на конечные точки ресурсов (даже если запрос использует авторизацию: Basic заголовок). Ошибка, которую вы получите:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
Я сделал 2 изменения, чтобы получить эту работу:
Во-первых, закажите WebSecurityConfigurerAdapter выше, чем сервер ресурсов (заказ 2 выше, чем заказ 3).
@Configuration
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
Во-вторых, пусть configure (HttpSecurity) использует клиент RequestMatcher, который соответствует только "Authorization: Basic".
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.anonymous().disable()
.requestMatcher(new BasicRequestMatcher())
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(oAuth2AuthenticationEntryPoint())
.and()
// ... other stuff
}
...
private static class BasicRequestMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
String auth = request.getHeader("Authorization");
return (auth != null && auth.startsWith("Basic"));
}
}
В результате он сопоставляет и обрабатывает запрос ресурса Basic Auth до того, как фильтр ресурса сервера имеет шанс сопоставить его. Он также ТОЛЬКО обрабатывает Authorizaiton: запрос базового ресурса, поэтому любые запросы с авторизацией: Bearer будет проваливаться, а затем обрабатываться фильтром filterChain (т.е. Фильтром Oauth). Кроме того, он находится ниже, чем AuthenticationServer (в случае, если AuthenticationServer включен в том же проекте), поэтому он не мешает процессу фильтрования AuthenticaitonServer обрабатывать запрос в/oauth/токен и т.д.
Ответ 2
Это может быть близко к тому, что вы искали:
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new OAuthRequestedMatcher())
.authorizeRequests()
.anyRequest().authenticated();
}
private static class OAuthRequestedMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
String auth = request.getHeader("Authorization");
// Determine if the client request contained an OAuth Authorization
return (auth != null) && auth.startsWith("Bearer");
}
}
Единственное, что не предусмотрено, это способ "отступить", если аутентификация не удалась.
Для меня этот подход имеет смысл. Если пользователь напрямую предоставляет аутентификацию для запроса через Basic auth, то OAuth не требуется. Если Клиент является действующим, нам нужен этот фильтр для входа и убедитесь, что запрос правильно аутентифицирован.
Ответ 3
Я считаю, что невозможно получить обе аутентификации. У вас может быть базовая аутентификация и аутентификация oauth2, но для разных конечных точек. Как и вы, первая конфигурация преодолеет вторую, в этом случае будет использоваться http basic.
Ответ 4
Вы можете добавить BasicAuthenticationFilter в цепочку фильтров безопасности, чтобы получить защиту OAuth2 OR Basic на защищенном ресурсе. Пример config ниже...
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManagerBean;
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
final String[] userEndpoints = {
"/v1/api/airline"
};
final String[] adminEndpoints = {
"/v1/api/jobs**"
};
http
.requestMatchers()
.antMatchers(userEndpoints)
.antMatchers(adminEndpoints)
.antMatchers("/secure/**")
.and()
.authorizeRequests()
.antMatchers("/secure/**").authenticated()
.antMatchers(userEndpoints).hasRole("USER")
.antMatchers(adminEndpoints).hasRole("ADMIN");
// @formatter:on
http.addFilterBefore(new BasicAuthenticationFilter(authenticationManagerBean),
UsernamePasswordAuthenticationFilter.class);
}
}
Ответ 5
Решение @kca2ply обеспечивает работу очень хорошо. Я заметил, что браузер не выдавал вызов, поэтому я немного изменил код:
@Configuration
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.anonymous().disable()
.requestMatcher(request -> {
String auth = request.getHeader(HttpHeaders.AUTHORIZATION);
return (auth != null && auth.startsWith("Basic"));
})
.antMatcher("/**")
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
// @formatter:on
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
Используя как requestMatcher()
, так и antMatcher()
, все работает отлично. Браузеры и HTTP-клиенты теперь будут оспаривать основные кредиты, если они уже не предоставлены. Если учетные данные не предоставлены, он переходит в OAuth2.
Ответ 6
Не могу предоставить вам полный пример, но здесь есть подсказки для копания:
Грубо говоря, spring auth - это просто комбинация фильтра запросов, который извлекает данные аутентификации из запроса (заголовки) и диспетчера аутентификации, который предоставляет объект аутентификации для этого auth.
Итак, чтобы получить базовый и oauth на том же URL-адресе, вам нужно установить 2 фильтра в цепочке фильтров BasicAuthenticationFilter и OAuth2AuthenticationProcessingFilter.
Я думаю, проблема в том, что ConfiguringAdapters хороши для более простых confs, поскольку они, как правило, переопределяют друг друга. Итак, как первый шаг, попробуйте переместить
.httpBasic();
вызов ResourceServerConfiguration
Обратите внимание, что вам также необходимо предоставить 2 разных менеджера auth: один для базового auth и один для oauth
Ответ 7
И почему бы не сделать это наоборот? Просто обходите сервер ресурсов, если нет подключенного токена, а затем отмените его на обычную цепочку фильтров безопасности. Это, кстати, то, что фильтр сервера ресурсов останавливается.
@Configuration
@EnableResourceServer
class ResourceServerConfig : ResourceServerConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(resources: ResourceServerSecurityConfigurer) {
resources.resourceId("aaa")
}
/**
* Resources exposed via oauth. As we are providing also local user interface they are also accessible from within.
*/
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.requestMatcher(BearerAuthorizationHeaderMatcher())
.authorizeRequests()
.anyRequest()
.authenticated()
}
private class BearerAuthorizationHeaderMatcher : RequestMatcher {
override fun matches(request: HttpServletRequest): Boolean {
val auth = request.getHeader("Authorization")
return auth != null && auth.startsWith("Bearer")
}
}
}