如何使用@WebMvcTest、MockMvc 和 OAuth Security 测试控制器

How can I test a Controller with @WebMvcTest, MockMvc and OAuth Security

我有一个 Spring Boot 2.4.5 项目,Kotlin 使用这个 example

创建
src/
├── main/
│   └── kotlin/
│       └── de.mbur.myapp/
│           ├── controller/
│           │   └── WebController.kt
│           ├── security/
│           │   └── WebSecurityConfiguration.kt
│           └── MyApplication.kt
└── test/
    └── kotlin/
        └── de.mbur.myapp/
            ├── controller/
            │   └── WebControllerTest.kt
            ├── security/
            └── MyApplicationTests.kt

安全配置:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class WebSecurityConfiguration(private val configurer: AADB2COidcLoginConfigurer) : WebSecurityConfigurerAdapter() {

  override fun configure(http: HttpSecurity) {
    http.authorizeRequests()
      .anyRequest().authenticated()
      .and()
      .apply(configurer)
  }
}

控制器:

@Controller
class WebController {
  private fun initializeModel(model: Model, token: OAuth2AuthenticationToken?) {
    if (token != null) {
      val user = token.principal
      model.addAllAttributes(user.attributes)
      model.addAttribute("grant_type", user.authorities)
      model.addAttribute("name", user.name)
    }
  }

  @GetMapping("/")
  fun index(model: Model, token: OAuth2AuthenticationToken?): String {
    initializeModel(model, token)
    return "home"
  }

}

最后是测试:

@WebMvcTest(WebController::class)
internal class WebControllerTest {

  @Autowired
  private lateinit var mockMvc: MockMvc

  @MockBean
  private lateinit var configurer: AADB2COidcLoginConfigurer

  @Test
  fun testWebController() {
    mockMvc
      .perform(get("/"))
      .andDo(print())
      .andExpect(status().isForbidden)
  }

  @Test
  @WithMockUser
  fun testAuthentication() {
    mockMvc
      .perform(get("/"))
      .andDo(print())
      .andExpect(status().isOk)
  }

}

首先我必须提到我必须模拟 AADB2COidcLoginConfigurer 才能进行测试 testWebController 运行。

然后我尝试使用 @WithMockUser 注释 运行 第二个测试,就像我现在从经典 spring 安全测试中那样。但这没有用:

Request processing failed; nested exception is java.lang.IllegalStateException: Current user principal is not of type [org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken]: UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]

当预期 OAuth2AuthenticationToken 时,我如何 运行 此测试类似于 Spring 用户名密码安全性?

您可以使用 Spring 安全提供的 RequestPostProcessors 之一。

fun testAuthentication() {
    mockMvc
      .perform(get("/")
            .with(oauth2Login())
      .andExpect(status().isOk)
  }

我找到并适应我的用例的替代解决方案是:

import org.springframework.security.test.context.support.WithSecurityContext

@Retention(AnnotationRetention.RUNTIME)
@WithSecurityContext(factory = WithMockOAuth2SecurityContextFactory::class)
annotation class WithMockOAuth2
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.security.oauth2.core.oidc.OidcIdToken
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser
import org.springframework.security.test.context.support.WithSecurityContextFactory


class WithMockOAuth2SecurityContextFactory : WithSecurityContextFactory<WithMockOAuth2?> {

  override fun createSecurityContext(mockOAuth2: WithMockOAuth2?): SecurityContext {
    val context = SecurityContextHolder.createEmptyContext()
    if (mockOAuth2 != null) {
      val authorities = null
      val idToken = OidcIdToken
        .withTokenValue("test")
        .claim("sub", "test")
        .build()
      val principal = DefaultOidcUser(authorities, idToken)
      val auth = OAuth2AuthenticationToken(principal, authorities, "test")
      context.authentication = auth
    }
    return context
  }
}
  @Test
  @WithMockOAuth2
  fun testAuthentication() {
    mockMvc

但是另一个更短并且工作正常...

如果需要测试特定的角色、范围或名称,这个可能更灵活...