У меня есть приложение Spring Boot (1.2.1.RELEASE), которое обслуживает авторизацию и сервер ресурсов OAuth2 (2.0.6.RELEASE) в одном экземпляре приложения. Он использует пользовательскую реализацию UserDetailsService
, которая использует MongoTemplate
для поиска пользователей в MongoDB. Аутентификация с помощью grant_type=password
на /oauth/token
работает как шарм, а также авторизация с заголовком Authorization: Bearer {token}
при вызове определенных ресурсов.
Теперь я хочу добавить на сервер простое диалоговое окно подтверждения OAuth, чтобы я мог аутентифицироваться и авторизироваться, например. Swagger UI вызывает api-docs для защищенных ресурсов. Вот что я сделал до сих пор:
@Configuration
@SessionAttributes("authorizationRequest")
class OAuth2ServerConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/oauth/confirm_access").setViewName("authorize");
}
@Configuration
@Order(2)
protected static class LoginConfig extends WebSecurityConfigurerAdapter implements ApplicationEventPublisherAware {
@Autowired
UserDetailsService userDetailsService
@Autowired
PasswordEncoder passwordEncoder
ApplicationEventPublisher applicationEventPublisher
@Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider()
provider.passwordEncoder = passwordEncoder
provider.userDetailsService = userDetailsService
return provider
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.parentAuthenticationManager(authenticationManagerBean())
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
//return super.authenticationManagerBean()
ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
return providerManager
}
@Bean
public PasswordEncoder passwordEncoder() {
new BCryptPasswordEncoder(5)
}
}
@Configuration
@EnableResourceServer
protected static class ResourceServer extends ResourceServerConfigurerAdapter {
@Value('${oauth.resourceId}')
private String resourceId
@Autowired
@Qualifier('authenticationManagerBean')
private AuthenticationManager authenticationManager
@Override
public void configure(HttpSecurity http) throws Exception {
http.setSharedObject(AuthenticationManager.class, authenticationManager)
http.csrf().disable()
http.httpBasic().disable()
http.formLogin().loginPage("/login").permitAll()
//http.authenticationProvider(daoAuthenticationProvider())
http.anonymous().and()
.authorizeRequests()
.antMatchers('/login/**').permitAll()
.antMatchers('/uaa/register/**').permitAll()
.antMatchers('/uaa/activate/**').permitAll()
.antMatchers('/uaa/password/**').permitAll()
.antMatchers('/uaa/account/**').hasAuthority('ADMIN')
.antMatchers('/api-docs/**').permitAll()
.antMatchers('/admin/**').hasAuthority('SUPERADMIN')
.anyRequest().authenticated()
//http.sessionManagement().sessionCreationPolicy(STATELESS)
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceId)
resources.authenticationManager(authenticationManager)
}
}
@Configuration
@EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Value('${oauth.clientId}')
private String clientId
@Value('${oauth.secret:}')
private String secret
@Value('${oauth.resourceId}')
private String resourceId
@Autowired
@Qualifier('authenticationManagerBean')
private AuthenticationManager authenticationManager
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
return new JwtAccessTokenConverter();
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.checkTokenAccess("permitAll()")
oauthServer.allowFormAuthenticationForClients()
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter())
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientId)
.secret(secret)
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("USER", "ADMIN")
.scopes("read", "write", "trust")
.resourceIds(resourceId)
}
}
}
Основная проблема заключается в том, что я не могу запустить оба (форма входа в систему и токен авторизации OAuth2 в заголовке). Если ResourceServer
получает более высокий приоритет, то авторизация авторизации OAuth2 работает, но я не могу войти в систему, используя веб-форму. С другой стороны, если я задаю более высокий приоритет классу LoginConfig
, то авторизация токена OAuth2 перестает работать.
Пример из практики: форма входа в систему, авторизация авторизации OAuth2 не
Я выяснил, что в этом случае проблема вызвана незарегистрированным OAuth2AuthenticationProcessingFilter
. Я попытался зарегистрировать его вручную в методе ResourceServer.configure(HttpSecurity http)
, но он не сработал - я мог видеть фильтр в списке FilterChain, но он не запускался. Это не был хороший способ исправить это, потому что во время инициализации ResourceServer появилось много другой магии, поэтому я перешел во второй случай.
Тематическое исследование: форма входа в систему не работает, авторизация авторизации OAuth2
В этом случае основная проблема заключается в том, что по умолчанию UsernamePasswordAuthenticationFilter
не может найти правильно сконфигурированный экземпляр AuthenticationProvider
(в ProviderManager
). Когда я попытался добавить его вручную, выполните следующие действия:
http.authenticationProvide(daoAuthenticationProvider())
он получает один, но в этом случае нет AuthenticationEventPublisher
. Определенная и успешная аутентификация не может быть опубликована другим компонентам. А на следующей итерации она заменяется на AnonymousAuthenticationToken
. Поэтому я попытался определить вручную AuthenticationManager
экземпляр с DaoAuthenticationProvider
внутри:
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
//return super.authenticationManagerBean()
ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
return providerManager
}
Я думал, что это сработает, но есть другая проблема с предоставлением экземпляра AuthenticationManager
для зарегистрированных фильтров. Оказывается, каждый фильтр имеет AuthenticationManager
, введенный вручную, используя компонент sharedObjects
:
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
Проблема заключается в том, что у вас нет гарантированного правильного набора экземпляров, потому что есть простой HashMap (проверить его на GitHub), используемый для хранения определенных общих объект, и он может быть изменен в любое время. Я попытался установить его в:
http.setSharedObject(AuthenticationManager.class, authenticationManager)
но прежде чем я доберусь до места, где он читается, он уже заменен по умолчанию. Я проверил его с помощью отладчика, и похоже, что для каждого нового фильтра есть новый экземпляр диспетчера проверки подлинности.
Мой вопрос: Я делаю это правильно? Как настроить сервер авторизации с сервером ресурсов, встроенным в одно приложение, с формой входа (диалог OAuth2)? Может быть, это можно сделать другим и намного проще. Я был бы благодарен за любую помощь.