¿Cómo habilitar la authentication de portador en la aplicación Spring Boot?

Lo que bash lograr es:

  • usuarios, autoridades, clientes y tokens de acceso almacenados en una database (es decir, MySQL) a los que se accede a través de jdbc
  • La API expone los puntos finales para que usted pueda preguntar "¿Puedo tener un token de portador OAuth2? Conozco el ID del cliente y el secreto"
  • API le permite acceder a los puntos finales MVC si proporciona un token de portador en su encabezado de request

Llegué bastante lejos con esto: los primeros dos puntos están funcionando.

No pude utilizar una configuration de OAuth2 completamente pnetworkingeterminada para mi aplicación Spring Boot, porque los nombres de las tablas estándar ya están en uso en mi database (ya tengo una tabla de "usuarios", por ejemplo).

Construí mis propias instancias de JdbcTokenStore, JdbcClientDetailsService y JdbcAuthorizationCodeServices manualmente, las configuré para usar los nombres de tabla personalizados de mi database y configuré mi aplicación para usar estas instancias.


Entonces, esto es lo que tengo hasta ahora. Puedo pedir un token de portador:

# 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 

Recibo una respuesta; ¡bonito!

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

Ahora trato de usar ese token:

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

Ay:

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

Esto significa (en este caso particular) que ha retrocedido a la authentication anónima . Puede ver el error real si agrego .anonymous().disable() a mi HttpSecurity:

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

Investigué esto más profundamente al boost la verbosidad de logging:

 logging.level: org.springframework: security: DEBUG 

Esto revela los 10 filters a través de los cuales viaja mi request:

 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 occurnetworking; networkingirecting to authentication entry point org.springframework.security.authentication.AuthenticationCnetworkingentialsNotFoundException: An Authentication object was not found in the SecurityContext at org.springframework.security.access.intercept.AbstractSecurityInterceptor.cnetworkingentialsNotFound(AbstractSecurityInterceptor.java:379) ~[spring-security-core-4.2.3.RELEASE.jar:4.2.3.RELEASE] 

Eso es lo que parece si los usuarios anónimos están deshabilitados . Si están habilitados : AnonymousAuthenticationFilter se agrega a la cadena de filters justo después de SecurityContextHolderAwareRequestFilter , y la secuencia termina más como esta:

 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; Cnetworkingentials: [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); networkingirecting 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] 

De cualquier manera: no es bueno.

Esencialmente, me indica que falta un paso en la cadena de filters. Necesitamos un filter que lea el encabezado de ServletRequest y complete la authentication del context de security:

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

Me pregunto cómo get un filter así?


Así es como se ve mi aplicación. Es Kotlin, pero espero que tenga sentido para el ojo de 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_networkingirect_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 = ?") } } 

¿Algunas ideas? ¿Podría ser que necesito instalar de alguna manera el OAuth2AuthenticationProcessingFilter en mi cadena de filters?

Recibo esos posts al inicio … ¿podrían estar relacionados con el problema?

 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. 

EDITAR:

Parece que instalar OAuth2AuthenticationProcessingFilter es el trabajo de ResourceServerConfigurerAdapter . He agregado la siguiente class:

MyResourceServerConfigurerAdapter:

 @Configuration @EnableResourceServer class MyResourceServerConfigurerAdapter : ResourceServerConfigurerAdapter() 

Y confirmo en el depurador que esto hace que ResourceServerSecurityConfigurer ingrese su método de configure(http: HttpSecurity) , que parece que intenta instalar un OAuth2AuthenticationProcessingFilter en la cadena de filters.

Pero no parece que haya tenido éxito. De acuerdo con la salida de debugging de Spring Security: todavía tengo la misma cantidad de filters en mi cadena de filters. OAuth2AuthenticationProcessingFilter no está allí. ¿Que esta pasando?


EDIT2 : Me pregunto si el problema es que tengo dos classs ( WebSecurityConfigurerAdapter , ResourceServerConfigurerAdapter ) que intentan configurar HttpSecurity. ¿Es mutuamente exclusivo?

    ¡Sí! El problema estaba relacionado con el hecho de que había registrado tanto un WebSecurityConfigurerAdapter como un ResourceServerConfigurerAdapter .

    Solución: elimine WebSecurityConfigurerAdapter . Y use este 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 } } 

    EDITAR : Para que la authentication de portador se aplique a todos los puntos finales (por ejemplo, el punto final /metrics instalado por Spring Actuator), descubrí que también tenía que agregar security.oauth2.resource.filter-order: 3 a mi application.yml . Vea esta respuesta .