Spring security jwtAuthConverter未被触发

beq87vna  于 2023-10-16  发布在  Spring
关注(0)|答案(1)|浏览(116)

我试图在我的Spring Boot 后端为我的认证系统使用 Spring 安全和keycloak。keycloak部分正在做它的工作,我有一个keycloak服务器运行在localhost:8180,我可以在那里创建角色等。当后端用户没有连接时,keycloak webview弹出,让我连接或注册一个新用户。当显示OAuth2 AuthenticationToken的inide时,我得到了所有正确的信息,包括用户的角色(屏幕截图1)。
问题是,当使用@preAuthorize()(AppController.kt)保护路由时,hasRole函数会触发hasAnyRole和hasAnyAuthorityName(屏幕截图2)。但是当查看roleSet时,调用(getAuthoritySet())我没有看到PREFIX_client_test。当在jwtAuthenticationCOnverter()中放置断点时,我发现它只在应用程序启动时触发,而不是在访问受保护的路由时触发。我不知道你是否有任何线索,我会提供给你的代码和截图下,谢谢你的回答。
OAuth2 AuthenticationToken Result of OAuth2AuthenticationToken的结果
hasAnyAuthorityName中的断点Breakpoint in hasAnyAuthorityName

AppController.kt

package com.linkedout.backend.controller

import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/")
open class AppController {
    @GetMapping
    open fun hello(token: OAuth2AuthenticationToken): String {
        return "Token: ${token}"
    }

    @GetMapping("candidate")
    @PreAuthorize("hasRole('client_candidate')")
    open fun helloProtected(): String {
        return "Hello premium user"
    }

    @GetMapping("test")
    @PreAuthorize("hasRole('client_test')")
    open fun test(): String {
        return "This is a test"
    }

    @GetMapping("admin")
    @PreAuthorize("hasRole('client_admin')")
    open fun helloProtectedAdmin(): String {
        return "Hello admin user"
    }
}

SecurityConfig.kt

package com.linkedout.backend.config

import com.linkedout.backend.KeycloakLogoutHandler
import lombok.RequiredArgsConstructor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
internal open class SecurityConfig() {

    @Bean
    @Throws(Exception::class)
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http.csrf { csrf -> csrf.disable() }
                .authorizeHttpRequests { auth -> auth.anyRequest().fullyAuthenticated() }
                .oauth2Login {  }
                .oauth2ResourceServer { oauth2 -> oauth2.jwt { jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter()) } }

        return http.build()
    }


    open fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val jwtGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles")
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_")

        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter)
        return jwtAuthenticationConverter
    }
}

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8180/realms/Linkedout
          jwk-set-uri: http://localhost:8180/realms/Linkedout/protocol/openid-connect/certs
      client:
        registration:
          keycloak:
            client-id: backend
            client-secret: PjkVtHOWmu7BEekOqJnY8TopHrJO0iCE
            authorization-grant-type: authorization_code
            scope: openid
        provider:
          keycloak:
            issuer-uri: http://localhost:8180/realms/LinkedOut
            user-name-attribute: preferred_username

server:
  port: 9090
2ledvvac

2ledvvac1#

根据文档,OAuth2客户端中的权限Map是使用GrantedAuthoritiesMapperOAuth2UserService完成的。在浏览器中使用oauth2Login时,JwtAuthenticationConverter或更一般的任何Converter<Jwt, ? extends AbstractAuthenticationToken>都不可能被调用:使用调试工具检查查询,您不会发现包含JWT的授权头(您将只有JSESSIONID cookie)。
您将OAuth2客户端和资源服务器配置添加到单个安全过滤器链中。这是一个坏主意,并导致你正在努力解决的那种困惑:

  • oauth2Login是客户关注的问题。它需要基于会话的安全性,因此应针对CSRF进行保护:使用oauth2Login在安全过滤器链中禁用CSRF保护是错误的。由这样的过滤器链构建的Authentication实现是OAuth2AuthenticationToken,就像你在运行时看到的那样(或者如果用户没有经过身份验证,则是AnonymousAuthenticationToken
  • JwtAuthenticationConverter由资源服务器的默认身份验证管理器使用:使用JWT访问令牌授权请求时(非会话),构建JwtAuthenticationToken(非OAuth2AuthenticationToken

您应该确定您的安全过滤器链是用于OAuth2客户端还是OAuth2资源服务器,并删除不符合您需要的部分。
仅作为REST API的应用程序可能只包含资源服务器配置。登录和注销不应该是它的责任:这应该由查询它的OAuth2客户端(Postman等工具或RestTemplate@FeignClientWebClientRestClient等REST客户端)处理。
如果您的应用程序需要既是OAuth2客户端又是OAuth2资源服务器,那么您应该定义两个不同的SecurityFilterChain bean:一个具有客户端配置(会话、CSRF保护、登录和注销),另一个具有资源服务器配置(无会话、无CSRF保护、无登录、无注销)。例如,在公开提供Thymeleaf模板(使用oauth2Login)的MVC应用程序和由OAuth2客户端(具有REST客户端的MVC控制器,或任何其他发送带有Bearer Authorization报头的REST请求的微服务)查询的REST API中,可能需要这一点。
请注意,根据最新的建议,OAuth2客户端(将查询发送到OAuth2资源服务器)应该是“机密的”,这要求它在您信任的服务器上运行。这不包括SPA和移动的应用程序(无法保密,这是许多教程中将其配置为“公共”客户端的原因)。对于这样的应用程序,可以使用BFF,例如配置为OAuth2客户端的spring-cloud-gateway,带有登录和TokenRelay过滤器(在将请求从前端转发到资源服务器之前,用包含会话中访问令牌的授权头替换会话cookie)。另一种选择是将REST API配置为OAuth2客户端,但这没有什么意义(为什么要费心发布和存储从未使用过的令牌),并且具有这种配置的团队可能应该质疑他们对OAuth2的使用(表单登录可能更适合)。
有关更多信息和示例配置,您可以找到我的教程有用。

相关问题