Как включить аутентификацию носителей в приложении Spring Boot?

Я пытаюсь достичь:

  • пользователей, полномочий, клиентов и токенов доступа, хранящихся в базе данных (например, MySQL), через jdbc
  • API предоставляет конечные точки, чтобы вы спросили: «Могу ли я иметь токен-маркер OAuth2? Я знаю идентификатор клиента и секрет»,
  • API позволяет вам обращаться к конечным точкам MVC, если вы подаете токен-носитель в свой заголовок запроса

Я довольно далеко справился с этим – первые два пункта работают.

Я не смог использовать полностью установленную OAuth2 настройку для моего приложения Spring Boot, потому что стандартные имена таблиц уже используются в моей базе данных (например, у меня уже есть таблица «пользователей»).

Я создал собственные экземпляры JdbcTokenStore, JdbcClientDetailsService и JdbcAuthorizationCodeServices вручную, настроил их на использование имен пользовательских таблиц из моей базы данных и настроил мое приложение для использования этих экземпляров.


Итак, вот что я до сих пор. Я могу попросить маркер-носитель:

# The `-u` switch provides the client ID & secret over HTTP Basic Auth curl -u8fc9d384-619a-11e7-9fe6-246798c61721:9397ce6c-619a-11e7-9fe6-246798c61721 \ 'http://localhost:8080/oauth/token' \ -d grant_type=password \ -d username=bob \ -d password=tom 

Я получаю ответ; хороший!

 {"access_token":"1ee9b381-e71a-4e2f-8782-54ab1ce4d140","token_type":"bearer","refresh_token":"8db897c7-03c6-4fc3-bf13-8b0296b41776","expires_in":26321,"scope":"read write"} 

Теперь я пытаюсь использовать этот токен:

 curl 'http://localhost:8080/test' \ -H "Authorization: Bearer 1ee9b381-e71a-4e2f-8782-54ab1ce4d140" 

Увы:

 { "timestamp":1499452163373, "status":401, "error":"Unauthorized", "message":"Full authentication is required to access this resource", "path":"/test" } 

Это означает (в данном конкретном случае), что он вернулся к анонимной аутентификации. Вы можете увидеть реальную ошибку, если я добавлю .anonymous().disable() в мой HttpSecurity:

 { "timestamp":1499452555312, "status":401, "error":"Unauthorized", "message":"An Authentication object was not found in the SecurityContext", "path":"/test" } 

Я исследовал это глубже, увеличив количество журналов:

 logging.level: org.springframework: security: DEBUG 

Это показывает 10 фильтров, через которые проходит мой запрос:

 ossecurity.web.FilterChainProxy : /test at position 1 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter' ossecurity.web.FilterChainProxy : /test at position 2 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter' wcHttpSessionSecurityContextRepository : No HttpSession currently exists wcHttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created. ossecurity.web.FilterChainProxy : /test at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter' ossecurity.web.FilterChainProxy : /test at position 4 of 10 in additional filter chain; firing Filter: 'LogoutFilter' ossecurity.web.FilterChainProxy : /test at position 5 of 10 in additional filter chain; firing Filter: 'BasicAuthenticationFilter' ossecurity.web.FilterChainProxy : /test at position 6 of 10 in additional filter chain; firing Filter: 'RequestCacheAwareFilter' ossecurity.web.FilterChainProxy : /test at position 7 of 10 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter' ossecurity.web.FilterChainProxy : /test at position 8 of 10 in additional filter chain; firing Filter: 'SessionManagementFilter' ossecurity.web.FilterChainProxy : /test at position 9 of 10 in additional filter chain; firing Filter: 'ExceptionTranslationFilter' ossecurity.web.FilterChainProxy : /test at position 10 of 10 in additional filter chain; firing Filter: 'FilterSecurityInterceptor' osswaiFilterSecurityInterceptor : Secure object: FilterInvocation: URL: /test; Attributes: [authenticated] osswaExceptionTranslationFilter : Authentication exception occurred; redirecting to authentication entry point org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:379) ~[spring-security-core-4.2.3.RELEASE.jar:4.2.3.RELEASE] 

Вот как это выглядит, если анонимные пользователи отключены . Если они включены : AnonymousAuthenticationFilter добавляется в цепочку фильтров сразу после SecurityContextHolderAwareRequestFilter , и последовательность заканчивается примерно так:

 ossecurity.web.FilterChainProxy : /test at position 11 of 11 in additional filter chain; firing Filter: 'FilterSecurityInterceptor' osswaiFilterSecurityInterceptor : Secure object: FilterInvocation: URL: /test; Attributes: [authenticated] osswaiFilterSecurityInterceptor : Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS ossaccess.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@5ff24abf, returned: -1 osswaExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point org.springframework.security.access.AccessDeniedException: Access is denied at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.2.3.RELEASE.jar:4.2.3.RELEASE] 

В любом случае: ничего хорошего.

По сути, это указывает на то, что мы пропускаем какой-то шаг в цепочке фильтров. Нам нужен фильтр, который будет читать заголовок ServletRequest, а затем заполнить аутентификацию контекста безопасности:

 SecurityContextHolder.getContext().setAuthentication(request: HttpServletRequest); 

Интересно, как получить такой фильтр?


Вот как выглядит мое приложение. Это Котлин, но, надеюсь, это должно иметь смысл для глаза Java.

Application.kt:

 @SpringBootApplication(scanBasePackageClasses=arrayOf( com.example.domain.Package::class, com.example.service.Package::class, com.example.web.Package::class )) class MyApplication fun main(args: Array<String>) { SpringApplication.run(MyApplication::class.java, *args) } 

TestController:

 @RestController class TestController { @RequestMapping("/test") fun Test(): String { return "hey there" } } 

MyWebSecurityConfigurerAdapter:

 @Configuration @EnableWebSecurity /** * Based on: * https://stackoverflow.com/questions/25383286/spring-security-custom-userdetailsservice-and-custom-user-class * * Password encoder: * http://www.baeldung.com/spring-security-authentication-with-a-database */ class MyWebSecurityConfigurerAdapter( val userDetailsService: MyUserDetailsService ) : WebSecurityConfigurerAdapter() { private val passwordEncoder = BCryptPasswordEncoder() override fun userDetailsService() : UserDetailsService { return userDetailsService } override fun configure(auth: AuthenticationManagerBuilder) { auth .authenticationProvider(authenticationProvider()) } @Bean fun authenticationProvider() : AuthenticationProvider { val authProvider = DaoAuthenticationProvider() authProvider.setUserDetailsService(userDetailsService()) authProvider.setPasswordEncoder(passwordEncoder) return authProvider } override fun configure(http: HttpSecurity?) { http!! .anonymous().disable() .authenticationProvider(authenticationProvider()) .authorizeRequests() .anyRequest().authenticated() .and() .httpBasic() .and() .csrf().disable() } } 

MyAuthorizationServerConfigurerAdapter:

 /** * Based on: * https://github.com/spring-projects/spring-security-oauth/blob/master/tests/annotation/jdbc/src/main/java/demo/Application.java#L68 */ @Configuration @EnableAuthorizationServer class MyAuthorizationServerConfigurerAdapter( val auth : AuthenticationManager, val dataSource: DataSource, val userDetailsService: UserDetailsService ) : AuthorizationServerConfigurerAdapter() { private val passwordEncoder = BCryptPasswordEncoder() @Bean fun tokenStore(): JdbcTokenStore { val tokenStore = JdbcTokenStore(dataSource) val oauthAccessTokenTable = "auth_schema.oauth_access_token" val oauthRefreshTokenTable = "auth_schema.oauth_refresh_token" tokenStore.setDeleteAccessTokenFromRefreshTokenSql("delete from ${oauthAccessTokenTable} where refresh_token = ?") tokenStore.setDeleteAccessTokenSql("delete from ${oauthAccessTokenTable} where token_id = ?") tokenStore.setDeleteRefreshTokenSql("delete from ${oauthRefreshTokenTable} where token_id = ?") tokenStore.setInsertAccessTokenSql("insert into ${oauthAccessTokenTable} (token_id, token, authentication_id, " + "user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)") tokenStore.setInsertRefreshTokenSql("insert into ${oauthRefreshTokenTable} (token_id, token, authentication) values (?, ?, ?)") tokenStore.setSelectAccessTokenAuthenticationSql("select token_id, authentication from ${oauthAccessTokenTable} where token_id = ?") tokenStore.setSelectAccessTokenFromAuthenticationSql("select token_id, token from ${oauthAccessTokenTable} where authentication_id = ?") tokenStore.setSelectAccessTokenSql("select token_id, token from ${oauthAccessTokenTable} where token_id = ?") tokenStore.setSelectAccessTokensFromClientIdSql("select token_id, token from ${oauthAccessTokenTable} where client_id = ?") tokenStore.setSelectAccessTokensFromUserNameAndClientIdSql("select token_id, token from ${oauthAccessTokenTable} where user_name = ? and client_id = ?") tokenStore.setSelectAccessTokensFromUserNameSql("select token_id, token from ${oauthAccessTokenTable} where user_name = ?") tokenStore.setSelectRefreshTokenAuthenticationSql("select token_id, authentication from ${oauthRefreshTokenTable} where token_id = ?") tokenStore.setSelectRefreshTokenSql("select token_id, token from ${oauthRefreshTokenTable} where token_id = ?") return tokenStore } override fun configure(security: AuthorizationServerSecurityConfigurer?) { security!!.passwordEncoder(passwordEncoder) } override fun configure(clients: ClientDetailsServiceConfigurer?) { val clientDetailsService = JdbcClientDetailsService(dataSource) clientDetailsService.setPasswordEncoder(passwordEncoder) val clientDetailsTable = "auth_schema.oauth_client_details" val CLIENT_FIELDS_FOR_UPDATE = "resource_ids, scope, " + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " + "refresh_token_validity, additional_information, autoapprove" val CLIENT_FIELDS = "client_secret, ${CLIENT_FIELDS_FOR_UPDATE}" val BASE_FIND_STATEMENT = "select client_id, ${CLIENT_FIELDS} from ${clientDetailsTable}" clientDetailsService.setFindClientDetailsSql("${BASE_FIND_STATEMENT} order by client_id") clientDetailsService.setDeleteClientDetailsSql("delete from ${clientDetailsTable} where client_id = ?") clientDetailsService.setInsertClientDetailsSql("insert into ${clientDetailsTable} (${CLIENT_FIELDS}," + " client_id) values (?,?,?,?,?,?,?,?,?,?,?)") clientDetailsService.setSelectClientDetailsSql("${BASE_FIND_STATEMENT} where client_id = ?") clientDetailsService.setUpdateClientDetailsSql("update ${clientDetailsTable} set " + "${CLIENT_FIELDS_FOR_UPDATE.replace(", ", "=?, ")}=? where client_id = ?") clientDetailsService.setUpdateClientSecretSql("update ${clientDetailsTable} set client_secret = ? where client_id = ?") clients!!.withClientDetails(clientDetailsService) } override fun configure(endpoints: AuthorizationServerEndpointsConfigurer?) { endpoints!! .authorizationCodeServices(authorizationCodeServices()) .authenticationManager(auth) .tokenStore(tokenStore()) .approvalStoreDisabled() .userDetailsService(userDetailsService) } @Bean protected fun authorizationCodeServices() : AuthorizationCodeServices { val codeServices = JdbcAuthorizationCodeServices(dataSource) val oauthCodeTable = "auth_schema.oauth_code" codeServices.setSelectAuthenticationSql("select code, authentication from ${oauthCodeTable} where code = ?") codeServices.setInsertAuthenticationSql("insert into ${oauthCodeTable} (code, authentication) values (?, ?)") codeServices.setDeleteAuthenticationSql("delete from ${oauthCodeTable} where code = ?") return codeServices } } 

MyAuthorizationServerConfigurerAdapter:

 @Service class MyUserDetailsService( val theDataSource: DataSource ) : JdbcUserDetailsManager() { @PostConstruct fun init() { dataSource = theDataSource val usersTable = "auth_schema.users" val authoritiesTable = "auth_schema.authorities" setChangePasswordSql("update ${usersTable} set password = ? where username = ?") setCreateAuthoritySql("insert into ${authoritiesTable} (username, authority) values (?,?)") setCreateUserSql("insert into ${usersTable} (username, password, enabled) values (?,?,?)") setDeleteUserAuthoritiesSql("delete from ${authoritiesTable} where username = ?") setDeleteUserSql("delete from ${usersTable} where username = ?") setUpdateUserSql("update ${usersTable} set password = ?, enabled = ? where username = ?") setUserExistsSql("select username from ${usersTable} where username = ?") setAuthoritiesByUsernameQuery("select username,authority from ${authoritiesTable} where username = ?") setUsersByUsernameQuery("select username,password,enabled from ${usersTable} " + "where username = ?") } } 

Есть идеи? Может быть, мне нужно как-то установить OAuth2AuthenticationProcessingFilter в мою цепочку фильтров?

Я получаю такие сообщения при запуске … Может ли это быть связано с проблемой?

 ucchsauth.MyUserDetailsService : No authentication manager set. Reauthentication of users when changing passwords will not be performed. scawcWebSecurityConfigurerAdapter$3 : No authenticationProviders and no parentAuthenticationManager defined. Returning null. 

РЕДАКТИРОВАТЬ:

Похоже, установка OAuth2AuthenticationProcessingFilter – это задача ResourceServerConfigurerAdapter . Я добавил следующий класс:

MyResourceServerConfigurerAdapter:

 @Configuration @EnableResourceServer class MyResourceServerConfigurerAdapter : ResourceServerConfigurerAdapter() 

И в отладчике я подтверждаю, что это приводит к тому, что ResourceServerSecurityConfigurer вводит свой метод configure(http: HttpSecurity) , который выглядит так, как будто он пытается установить OAuth2AuthenticationProcessingFilter в цепочку фильтров.

Но похоже, что это не удалось. Согласно отладке Spring Security: у меня все равно есть такое же количество фильтров в моей цепочке фильтров. OAuth2AuthenticationProcessingFilter не существует. Что происходит?


EDIT2 : Интересно, проблема в том, что у меня есть два класса ( WebSecurityConfigurerAdapter , ResourceServerConfigurerAdapter ), пытающиеся настроить HttpSecurity. Является ли это взаимоисключающим?

    Да! Проблема была связана с тем, что я зарегистрировал как WebSecurityConfigurerAdapter и ResourceServerConfigurerAdapter .

    Решение: удалите WebSecurityConfigurerAdapter . И используйте этот ResourceServerConfigurerAdapter :

     @Configuration @EnableResourceServer class MyResourceServerConfigurerAdapter( val userDetailsService: MyUserDetailsService ) : ResourceServerConfigurerAdapter() { private val passwordEncoder = BCryptPasswordEncoder() override fun configure(http: HttpSecurity?) { http!! .authenticationProvider(authenticationProvider()) .authorizeRequests() .anyRequest().authenticated() .and() .httpBasic() .and() .csrf().disable() } @Bean fun authenticationProvider() : AuthenticationProvider { val authProvider = DaoAuthenticationProvider() authProvider.setUserDetailsService(userDetailsService) authProvider.setPasswordEncoder(passwordEncoder) return authProvider } } 

    РЕДАКТИРОВАТЬ : для того, чтобы заставить атрибут Bearer применяться ко всем конечным точкам (например, конечная точка /metrics установленная приводом Spring), я обнаружил, что мне также нужно добавить security.oauth2.resource.filter-order: 3 в мой application.yml , См. Этот ответ .