如何在 Spring Boot 应用程序上启用 Bearer 身份验证?

Posted

技术标签:

【中文标题】如何在 Spring Boot 应用程序上启用 Bearer 身份验证?【英文标题】:How to enable Bearer authentication on Spring Boot application? 【发布时间】:2017-12-12 04:14:25 【问题描述】:

我想要实现的是:

存储在通过 jdbc 访问的数据库(即 mysql)中的用户、权限、客户端和访问令牌 API 公开端点以供您询问“我可以拥有 OAuth2 不记名令牌吗?我知道客户端 ID 和密码” 如果您在请求标头中提供 Bearer 令牌,API 允许您访问 MVC 端点

我在这方面已经走得很远了——前两点是有效的。

我无法为我的 Spring Boot 应用程序使用完全默认的 OAuth2 设置,因为标准表名称已在我的数据库中使用(例如,我已经有一个“用户”表)。

我手动构建了自己的 JdbcTokenStore、JdbcClientDetailsS​​ervice 和 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,您会看到 real 错误:


   "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 个过滤器:

o.s.security.web.FilterChainProxy        : /test at position 1 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
o.s.security.web.FilterChainProxy        : /test at position 2 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
o.s.security.web.FilterChainProxy        : /test at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
o.s.security.web.FilterChainProxy        : /test at position 4 of 10 in additional filter chain; firing Filter: 'LogoutFilter'
o.s.security.web.FilterChainProxy        : /test at position 5 of 10 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
o.s.security.web.FilterChainProxy        : /test at position 6 of 10 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
o.s.security.web.FilterChainProxy        : /test at position 7 of 10 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
o.s.security.web.FilterChainProxy        : /test at position 8 of 10 in additional filter chain; firing Filter: 'SessionManagementFilter'
o.s.security.web.FilterChainProxy        : /test at position 9 of 10 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
o.s.security.web.FilterChainProxy        : /test at position 10 of 10 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
o.s.s.w.a.i.FilterSecurityInterceptor    : Secure object: FilterInvocation: URL: /test; Attributes: [authenticated]
o.s.s.w.a.ExceptionTranslationFilter     : 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 之后,序列结束时更像这样:

o.s.security.web.FilterChainProxy        : /test at position 11 of 11 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
o.s.s.w.a.i.FilterSecurityInterceptor    : Secure object: FilterInvocation: URL: /test; Attributes: [authenticated]
o.s.s.w.a.i.FilterSecurityInterceptor    : 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
o.s.s.access.vote.AffirmativeBased       : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@5ff24abf, returned: -1
o.s.s.w.a.ExceptionTranslationFilter     : 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);

不知道如何获得这样的过滤器?


这就是我的应用程序的样子。它是 Kotlin,但希望它对 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://***.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 安装到我的过滤器链中?

我确实在启动时收到这样的消息……这些可能与问题有关吗?

u.c.c.h.s.auth.MyUserDetailsService      : No authentication manager set. Reauthentication of users when changing passwords will not be performed.
s.c.a.w.c.WebSecurityConfigurerAdapter$3 : No authenticationProviders and no parentAuthenticationManager defined. Returning null.

编辑:

看起来安装OAuth2AuthenticationProcessingFilterResourceServerConfigurerAdapter 的工作。我添加了以下类:

MyResourceServerConfigurerAdapter:

@Configuration
@EnableResourceServer
class MyResourceServerConfigurerAdapter : ResourceServerConfigurerAdapter()

我在调试器中确认这会导致 ResourceServerSecurityConfigurer 进入其 configure(http: HttpSecurity) 方法,确实看起来它试图将 OAuth2AuthenticationProcessingFilter 安装到过滤器链中。

但它看起来并不成功。根据 Spring Security 的调试输出:我的过滤器链中仍然有相同数量的过滤器。 OAuth2AuthenticationProcessingFilter 不在里面。怎么回事?


EDIT2:我想知道问题是否在于我有 两个 类(WebSecurityConfigurerAdapterResourceServerConfigurerAdapter)试图配置 HttpSecurity。是互斥的吗?

【问题讨论】:

auth0.com/blog/securing-spring-boot-with-jwts这个链接帮助你 【参考方案1】:

是的!问题与我注册两个 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
    


EDIT:为了让 Bearer auth 应用于 所有 端点(例如 Spring Actuator 安装的 /metrics 端点),我发现我必须还将security.oauth2.resource.filter-order: 3 添加到我的application.yml。见this answer。

【讨论】:

以上是关于如何在 Spring Boot 应用程序上启用 Bearer 身份验证?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring Boot 应用程序中启用 HTTPS,在 Tomcat 中部署为 WAR 文件?

如何在 Spring Boot 中在 Spring Security 级别启用 CORS [关闭]

如何在 Spring Boot / Spring Data 中为 Amazon RDS Mysql 启用 SSL?

如何启用Spring Boot Actuator的所有Endpoints

如何在 Spring Boot 库中启用 H2 控制台?

如何解决在使用 Spring Boot 和 Spring Security 启用 CSRF 后无法正常工作的登录问题?