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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/44977972/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 08:26:23  来源:igfitidea点击:

How to enable Bearer authentication on Spring Boot application?

javaspring-bootspring-securitykotlinspring-security-oauth2

提问by Birchlabs

What I am trying to achieve is:

我想要实现的是:

  • users, authorities, clients and access tokens stored in a database (i.e. MySQL) accessed via jdbc
  • API exposes endpoints for you to ask "can I have an OAuth2 bearer token? I know the client ID and secret"
  • API lets you access MVC endpoints if you supply a Bearer token in your request header
  • 存储在通过 jdbc 访问的数据库(即 MySQL)中的用户、权限、客户端和访问令牌
  • API 公开端点供您询问“我可以拥有 OAuth2 不记名令牌吗?我知道客户端 ID 和密码”
  • 如果您在请求标头中提供不记名令牌,则 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 usethat 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 anonymousauthentication. You can see the realerror if I add .anonymous().disable()to my HttpSecurity:

这意味着(在这种特殊情况下)它已经回退到匿名身份验证。如果我添加到我的 HttpSecurity,你会看到真正的错误.anonymous().disable()

{
   "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: AnonymousAuthenticationFilteris 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:

应用程序.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:

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 OAuth2AuthenticationProcessingFilterinto 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 : No authenticationProviders and no parentAuthenticationManager defined. Returning null.


EDIT:

编辑:

It looks like installing OAuth2AuthenticationProcessingFilteris the job of a ResourceServerConfigurerAdapter. I have added the following class:

看起来安装OAuth2AuthenticationProcessingFilterResourceServerConfigurerAdapter. 我添加了以下类:

MyResourceServerConfigurerAdapter:

MyResourceServerConfigurerAdapter:

@Configuration
@EnableResourceServer
class MyResourceServerConfigurerAdapter : ResourceServerConfigurerAdapter()

And I confirm in the debugger that this causes ResourceServerSecurityConfigurerto enter its configure(http: HttpSecurity)method, which doeslook like it tries to install a OAuth2AuthenticationProcessingFilterinto 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. OAuth2AuthenticationProcessingFilteris not in there. What's going on?

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



EDIT2: I wonder if the problem is that I have twoclasses (WebSecurityConfigurerAdapter, ResourceServerConfigurerAdapter) trying to configure HttpSecurity. Is it mutually exclusive?

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

采纳答案by Birchlabs

Yes! The problem was related to the fact that I had registered botha WebSecurityConfigurerAdapteranda ResourceServerConfigurerAdapter.

是的!问题与我同时注册aWebSecurityConfigurerAdaptera的事实有关ResourceServerConfigurerAdapter

Solution: delete the WebSecurityConfigurerAdapter. And use this 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: In order to get Bearer auth to apply to allendpoints (for example the /metricsendpoint installed by Spring Actuator), I found that I had to also add security.oauth2.resource.filter-order: 3to my application.yml. See this answer.

编辑:为了让 Bearer auth 应用于所有端点(例如/metricsSpring Actuator 安装的端点),我发现我还必须添加security.oauth2.resource.filter-order: 3到我的application.yml. 看到这个答案