我已经实现了一个控制器,它在服务器端(Sping Boot )使用JWT进行身份验证,该JWT是客户端通过Apple登录获得的。问题是我不知道如何测试接收到的JWT的算法,受众,issue-uri等是否正确。
我可以通过身份验证来存根JWT并检查状态200,但相反,我不能编写一个在错误的JWT情况下返回状态401的测试。(例如,一个确认收到JWT的测试,但由于issue-uri不正确而返回401)。
下面是环境和实现。
build.gradle.kts
plugins {
id("org.springframework.boot") version "3.1.4"
id("io.spring.dependency-management") version "1.1.3"
id("org.jetbrains.kotlin.plugin.jpa") version "1.8.22"
kotlin("jvm") version "1.8.22"
kotlin("plugin.spring") version "1.8.22"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("com.auth0:java-jwt:4.4.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")
testImplementation("junit:junit:4.13.2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test:6.1.5")
testImplementation("com.ninja-squad:springmockk:4.0.2")
testImplementation("org.wiremock:wiremock:3.3.1")
}
字符串
src/main/resources/application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://appleid.apple.com/auth/keys
issuer-uri: https://appleid.apple.com
audiences: ${APPLE_CLIENT_ID}
jws-algorithms: ${JWS_ALGORITHMS}
型
src/test/resources/application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://expected.com/keys
issuer-uri: https://expected.com
audiences: expected audiences
jws-algorithms: RS256
型
SecurityServerConfig.kt
@Configuration
class OAuth2ResourceServerSecurityConfig(
private val tokenService: DefaultTokenService
) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf {
it.ignoringRequestMatchers("/api/**")
}
.authorizeHttpRequests {
it.requestMatchers(
"/api/token/apple/code"
).authenticated()
it.anyRequest().permitAll()
}
.oauth2ResourceServer {
it.jwt {}
}
.authenticationManager {
val bearerToken = it as BearerTokenAuthenticationToken
val user = tokenService.parseToken(bearerToken.token)
?: throw InvalidBearerTokenException("Invalid token")
UsernamePasswordAuthenticationToken(
user,
"",
listOf(SimpleGrantedAuthority("USER"))
)
}
return http.build()
}
}
型
DefaultTokenService.kt
@Service
class DefaultTokenService(
private val jwtDecoder: JwtDecoder,
) : TokenService {
override fun parseToken(bearerToken: String): AppleUser? {
return try {
val jwt = jwtDecoder.decode(bearerToken)
val userId = jwt.claims["sub"] as String
val email = jwt.claims["email"] as String
DefaultAppleUser(userId, email)
} catch (e: Exception) {
null
}
}
// more implements...
型
TokenController.kt
@RestController
@RequestMapping("/api/token")
class TokenController(
private val tokenService: TokenService
) {
@GetMapping("/apple/code")
fun getRefreshTokenOfApple(
authentication: Authentication,
@RequestParam authorizationCode: String,
): AppleTokenResponse? {
return tokenService.getRefreshToken(authorizationCode)
}
// more implements...
}
型
这是控制器测试
TokenControllerTests.kt
@SpringBootTest
@AutoConfigureMockMvc
class TokenControllerTest{
@Autowired
private lateinit var mockMvc: MockMvc
@SpykBean
private lateinit var spyStubTokenService: DefaultTokenService
@Nested
inner class GetRefreshTokenOfApple {
@Test
fun `when given jwt is invalid, status is unauthorized`() {
val result = mockMvc.perform(
MockMvcRequestBuilders
.get("/api/token/apple/code")
.param("authorizationCode", "authorization code 1")
.with(csrf())
.with(jwt().jwt {
it.issuer("https://invalid.com")
it.audience(listOf("com.invalid.app"))
it.header("alg", "RS255")
})
)
result.andExpect(MockMvcResultMatchers.status().isUnauthorized)
}
@Test
fun `status is 200 and passes correct argument to token service`() {
val result = mockMvc.perform(
MockMvcRequestBuilders
.get("/api/token/apple/code")
.param("authorizationCode", "authorization code 1")
.with(jwt().jwt {
it.issuer("https://expected.com")
it.audience(listOf("com.expected.app"))
it.header("alg", "RS256")
})
)
result.andExpect(MockMvcResultMatchers.status().isOk)
verify {
spyStubTokenService.getRefreshToken(
"authorization code 1"
)
}
}
}
型
第一个预期未授权的测试将失败,因为它将返回状态200。我们希望确保正确检查JWT内容,并测试JWT身份验证是否正确完成。
我想最好能在src/test/resources/application.yml
中测试spring.security.oauth2.resourceserver.jwt
作为期望值。这是通常不需要首先测试的东西吗?
如果有人知道一个更好的方法来做到这一点,我会很感激。
(我已经试过了)
1. JwtDecoder
,带@SpykBean
注解。
@SpykBean
private late init var jwtDecoder: JwtDecoder
型
我发现这样可以在测试中始终通过认证,但不能擅自测试。
**2.我根据this article**编写了一个测试,但最终无法正确测试未经授权的内容。
2条答案
按热度按时间jucafojl1#
我认为这实际上是一个很好的问题,因为开发人员应该能够经常测试他们的API的所有安全条件。这应该确保技术安全性,例如只允许有效的发行者,以及JWT验证后的授权条件。这些操作还确保API是以客户端为中心的,例如它返回有用的OAuth错误响应。
与模拟JWT访问令牌的集成测试
使用您提到的mock技术,必须使用与真实的令牌相同的
access token contract
。这应该使用相同的JWT标头和有效负载值。API中的安全代码不应更改。相反,API仅指向mock JSON Web Key Set(JWKS)URI。然后,您可以编写这样的代码,以测试API身份验证和API授权条件,并Assert客户端的预期错误响应。每个API测试都可以快速获取任何用户的令牌,因此开发人员的设置是高效的。
字符串
示例代码
要进行比较,可以运行我的these example tests。他们使用mock authorization server class来处理技术设置,JWT库做较低级别的工作。下面是我的示例API的测试结果:
的数据
其他测试技术
当然,测试API安全性还有很多其他方法,不同的开发人员有不同的偏好。通常使用单元测试进行大多数测试。但就我个人而言,我也喜欢进行一些能够发出真实的HTTP请求的测试,以证明端到端基础设施正在按预期工作。
ig9co6j12#
MockMvc
适用于“单元”测试,但您尝试做的更多是“端到端”测试:您希望测试外部授权服务器交付的真实的JWT是否被您没有编写的JWT解码器接受或拒绝(您只是配置了它)。使用
MockMvc
,访问令牌解析和解码(或内省)的整个过程都被跳过:MockMvc请求后处理器被设计为使用身份验证管理器的结果(Authentication
示例,默认情况下,对于具有JWT解码器的资源服务器来说是JwtAuthenticationToken
)填充测试安全上下文。关于
MockMvc
的安全性,你应该“单元”测试的是访问控制规则。