You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让OAuth2资源服务器每次请求都与授权服务器校验?

如何让OAuth2资源服务器每次请求都校验令牌

我搭建了OAuth2授权服务器和资源服务器,目前资源服务器仅在首次请求时与授权服务器进行令牌校验,即使关闭授权服务器,资源服务器仍能正常处理请求。请问有没有办法让资源服务器每次请求都与授权服务器校验令牌?


授权服务器配置

@EnableWebSecurity
@Configuration
class WebSecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .cors { cors -> cors.disable() }
            .csrf { csrf -> csrf.disable() }
            .authorizeRequests().anyRequest().authenticated()
        http.formLogin()
        return http.build()
    }

    @Bean
    fun passwordEncoder() = BCryptPasswordEncoder()

}

@Configuration
class AuthServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    fun clientSecurityFilterChain(http: HttpSecurity): SecurityFilterChain? {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
        return http.formLogin().and().build()
    }

    @Bean
    fun providerSettings(): ProviderSettings = ProviderSettings
        .builder()
        .issuer("http://localhost:8283")
        .build()

    @Bean
    fun jwkSource(): JWKSource<SecurityContext> {
        val rsaKey = generateRsaKey()
        val set = JWKSet(rsaKey)
        return JWKSource { selector: JWKSelector, _: SecurityContext? ->
            selector.select(set)
        }
    }

    private fun generateRsaKey(): RSAKey {
        val keyPair = KeyPairGenerator.getInstance("RSA")
        keyPair.initialize(2048)
        val keys = keyPair.generateKeyPair()
        return try {
            RSAKey.Builder(keys.public as RSAPublicKey).privateKey(keys.private)
                .keyID(UUID.randomUUID().toString()).build()
        } catch (e: NoSuchAlgorithmException) {
            throw RuntimeException("Error while generating rsa keys")
        }
    }
}

UserDetailsService 与 RegisteredClientRepository

@Component
class CustomUserDetailsService(
    private val authUserRepository: AuthUserRepository
) : UserDetailsService {

    override fun loadUserByUsername(username: String) = authUserRepository.findByEmailAndDeletedFalse(username)

}

@Component
class CustomRegisteredClientRepository(
    private val clientRepository: ClientRepository,
    private val encoder: BCryptPasswordEncoder
) : RegisteredClientRepository {

    override fun save(registeredClient: RegisteredClient?) {
        registeredClient?.run {
            val client = CustomRegisteredClient(
                clientId,
                clientIdIssuedAt?.toEpochMilli(),
                encoder.encode(clientSecret),
                clientSecretExpiresAt?.toEpochMilli(),
                clientName,
                clientAuthenticationMethods.map { it.value },
                authorizationGrantTypes.map { it.value },
                redirectUris.toList(),
                scopes.toList(),
                clientSettings.isRequireAuthorizationConsent,
                tokenSettings.accessTokenTimeToLive,
                tokenSettings.refreshTokenTimeToLive
            )
            clientRepository.save(client)
        }

    }

    override fun findById(id: String?): RegisteredClient? {
        id?.let {
            val client = clientRepository.findById(it)
            if (client.isPresent) return client.get().toRegisteredClient()
        }
        return null
    }

    override fun findByClientId(clientId: String?): RegisteredClient? {
        clientId?.let {
            val client = clientRepository.findByClientId(clientId)
            return client?.toRegisteredClient()
        }
        return null
    }
}

// 初始化用户与客户端,保存至MongoDB

@Component
class ContextRefreshEvent(
    private val clientRepository: ClientRepository,
    private val passwordEncoder: BCryptPasswordEncoder,
    private val userRepository: AuthUserRepository
) {

    @EventListener(ContextRefreshedEvent::class)
    fun contextRefreshedEvent() {
        if (clientRepository.findByClientName("demo") == null) {
            val client = CustomRegisteredClient(
                "demo",
                Date().time,
                passwordEncoder.encode("12345"),
                Date().time.plus(3600),
                "demo",
                listOf(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.value),
                listOf(AuthorizationGrantType.AUTHORIZATION_CODE.value, AuthorizationGrantType.REFRESH_TOKEN.value),
                listOf("http://127.0.0.1:3000/authorized"),
                listOf(OidcScopes.OPENID),
                true,
                Duration.ofHours(1),
                Duration.ofHours(3),
            )
            clientRepository.save(client)
            println("====== 授权客户端已初始化 ======")
        }
        if (userRepository.findByEmailAndDeletedFalse("botir") == null) {
            val user = AuthUser(
                "minfin",
                passwordEncoder.encode("12345"),
                "botir",
                true,
                UserRole.ADMIN
            )
            userRepository.save(user)
            println("用户已初始化")
        }

    }
}

客户端应用(资源服务器)安全配置

@Configuration
class WebSecurityConfig {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .cors { cors -> cors.disable() }
            .csrf { csrf -> csrf.disable() }
            .sessionManagement { session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
            .oauth2ResourceServer { server ->
                server.jwt().jwkSetUri("http://localhost:8283/oauth2/jwks")
            }
            .authorizeRequests { auth ->
                auth.anyRequest().authenticated()
            }
        return http.build()
    }
}


@RestController
class TestController {

    @GetMapping
    fun test() = "Test ..."
}

解决方案

当前资源服务器使用的是JWT本地验证:它会从授权服务器的JWKS端点获取公钥并缓存,之后所有令牌校验都在本地完成,无需再请求授权服务器,因此关闭授权服务器后仍能正常工作。

要实现每次请求都与授权服务器校验,需要改用**令牌内省(Token Introspection)**机制,让资源服务器每次都向授权服务器的内省端点发送请求,验证令牌的有效性(包括是否被撤销)。具体步骤如下:

1. 确认授权服务器启用内省端点

Spring Authorization Server默认已启用内省端点(/oauth2/introspect),只需确保资源服务器对应的客户端配置了合法的认证信息(clientId、clientSecret),且客户端权限允许访问内省端点。

2. 修改资源服务器配置为不透明令牌验证

替换原有的JWT配置,改为使用opaqueToken配置,指向授权服务器的内省端点,并配置资源服务器作为客户端的认证信息:

@Configuration
class WebSecurityConfig {

    @Value("\${auth-server.introspect-uri}")
    private lateinit var introspectUri: String

    @Value("\${resource-server.client-id}")
    private lateinit var clientId: String

    @Value("\${resource-server.client-secret}")
    private lateinit var clientSecret: String

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .cors { cors -> cors.disable() }
            .csrf { csrf -> csrf.disable() }
            .sessionManagement { session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
            .oauth2ResourceServer { server ->
                server.opaqueToken { opaque ->
                    // 配置令牌内省端点
                    opaque.introspectionUri(introspectUri)
                    // 配置资源服务器向授权服务器认证的客户端信息
                    opaque.introspectionClientCredentials(clientId, clientSecret)
                }
            }
            .authorizeRequests { auth ->
                auth.anyRequest().authenticated()
            }
        return http.build()
    }
}

3. 添加配置文件参数

在资源服务器的application.yml中添加以下配置:

auth-server:
  introspect-uri: http://localhost:8283/oauth2/introspect
resource-server:
  client-id: demo
  client-secret: 12345

原理说明

使用不透明令牌验证时,资源服务器会将接收到的令牌发送到授权服务器的/oauth2/introspect端点,授权服务器返回令牌的详细有效性信息。每次请求都会触发该内省请求,因此只要授权服务器不可用,资源服务器就无法完成校验,会直接拒绝请求。


内容的提问来源于stack exchange,提问作者Botir Adoshboyev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 06:30:56