简体   繁体   English

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

[英]How to enable Bearer authentication on Spring Boot application?

What I am trying to achieve is: 我想要实现的是:

  • users, authorities, clients and access tokens stored in a database (ie MySQL) accessed via jdbc 存储在通过jdbc访问的数据库(即MySQL)中的用户,权限,客户端和访问令牌
  • API exposes endpoints for you to ask "can I have an OAuth2 bearer token? I know the client ID and secret" API公开了端点,让您问“我可以拥有OAuth2不记名令牌吗?我知道客户端ID和密码”
  • API lets you access MVC endpoints if you supply a Bearer token in your request header 如果您在请求标头中提供Bearer令牌,则API允许您访问MVC端点

I got pretty far with this — the first two points are working. 我对此非常了解 - 前两点正在发挥作用。

I was not able to use a completely default OAuth2 setup for my Spring Boot application, because the standard table names are already in-use in my database (I have a "users" table already, for example). 我无法为我的Spring Boot应用程序使用完全默认的OAuth2设置,因为标准表名已经在我的数据库中使用(例如,我已经有一个“用户”表)。

I constructed my own instances of JdbcTokenStore, JdbcClientDetailsService, and JdbcAuthorizationCodeServices manually, configured them to use the custom table names from my database, and set up my application to use these instances. 我手动构建了自己的JdbcTokenStore,JdbcClientDetailsS​​ervice和JdbcAuthorizationCodeServices实例,将它们配置为使用我的数据库中的自定义表名,并设置我的应用程序以使用这些实例。


So, here's what I have so far. 所以,这就是我到目前为止所拥有的。 I can ask for a Bearer token: 我可以要求持票人令牌:

# 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

I receive a response; 我收到了回复; nice! 太好了!

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

Now I try to use that token: 现在我尝试使用该令牌:

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

Alas: 唉:

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

This means (in this particular case) that it has fallen back to anonymous authentication. 这意味着(在这种特殊情况下)它已经回归到匿名身份验证。 You can see the real error if I add .anonymous().disable() to my HttpSecurity: 如果我将.anonymous().disable()到我的HttpSecurity,你可以看到真正的错误:

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

I investigated this more deeply by increasing the logging verbosity: 我通过增加日志记录的详细程度对此进行了更深入的调查:

logging.level:
    org.springframework:
      security: DEBUG

This reveals the 10 filters through which my request travels: 这揭示了我的请求通过的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]

That's what it looks like if Anonymous users are disabled . 如果匿名用户被禁用,那就是它的样子。 If they're enabled : AnonymousAuthenticationFilter is added into the filter chain just after SecurityContextHolderAwareRequestFilter , and the sequence ends more like this: 如果它们被启用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]

Either way: no good. 无论哪种方式:没有好处。

Essentially it indicates to me that we are missing some step in the filter chain. 从本质上讲,它向我表明我们在过滤器链中缺少一些步骤。 We need a filter that would read the header of the ServletRequest, then populate the security context's authentication: 我们需要一个过滤器来读取ServletRequest的头部,然后填充安全上下文的身份验证:

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

I wonder how to get such a filter? 我想知道如何获得这样的过滤器?


This is what my application looks like. 这就是我的应用程序的样子。 It's Kotlin, but hopefully it should make sense to the Java eye. 这是Kotlin,但希望它对Java眼睛有意义。

Application.kt: 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: 的TestController:

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

MyWebSecurityConfigurerAdapter: 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: 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: 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 = ?")
    }
}

Any ideas? 有任何想法吗? Could it be that I need to somehow install the OAuth2AuthenticationProcessingFilter into my filter chain? 难道我需要以某种方式将OAuth2AuthenticationProcessingFilter安装到我的过滤器链中吗?

I do get such messages on startup… could these be related to the problem? 我确实在启动时收到了这样的消息......这些可能与问题有关吗?

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.

EDIT: 编辑:

It looks like installing OAuth2AuthenticationProcessingFilter is the job of a ResourceServerConfigurerAdapter . 看起来安装OAuth2AuthenticationProcessingFilterResourceServerConfigurerAdapter的工作。 I have added the following class: 我添加了以下类:

MyResourceServerConfigurerAdapter: MyResourceServerConfigurerAdapter:

@Configuration
@EnableResourceServer
class MyResourceServerConfigurerAdapter : ResourceServerConfigurerAdapter()

And I confirm in the debugger that this causes ResourceServerSecurityConfigurer to enter its configure(http: HttpSecurity) method, which does look like it tries to install a OAuth2AuthenticationProcessingFilter into the filter chain. 我确认,这导致调试ResourceServerSecurityConfigurer进入其configure(http: HttpSecurity)方法, 看起来像它试图将安装OAuth2AuthenticationProcessingFilter到过滤器链。

But it doesn't look like it succeeded. 但它看起来并不成功。 According to Spring Security's debug output: I still have the same number of filters in my filter chain. 根据Spring Security的调试输出:我的过滤器链中仍然有相同数量的过滤器。 OAuth2AuthenticationProcessingFilter is not in there. OAuth2AuthenticationProcessingFilter不在那里。 What's going on? 这是怎么回事?


EDIT2 : I wonder if the problem is that I have two classes ( WebSecurityConfigurerAdapter , ResourceServerConfigurerAdapter ) trying to configure HttpSecurity. EDIT2 :我想知道问题是我有两个类( WebSecurityConfigurerAdapterResourceServerConfigurerAdapter )试图配置HttpSecurity。 Is it mutually exclusive? 它是互相排斥的吗?

Yes! 是! The problem was related to the fact that I had registered both a WebSecurityConfigurerAdapter and a ResourceServerConfigurerAdapter . 这个问题是关系到我注册了一个双方的事实WebSecurityConfigurerAdapter ResourceServerConfigurerAdapter

Solution: delete the WebSecurityConfigurerAdapter . 解决方案:删除WebSecurityConfigurerAdapter And use this ResourceServerConfigurerAdapter : 并使用此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 : In order to get Bearer auth to apply to all endpoints (for example the /metrics endpoint installed by Spring Actuator), I found that I had to also add security.oauth2.resource.filter-order: 3 to my application.yml . 编辑 :为了让Bearer auth应用于所有端点(例如Spring Actuator安装的/metrics端点),我发现我还必须将security.oauth2.resource.filter-order: 3添加到我的application.yml See this answer . 看到这个答案

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM