![](/img/trans.png)
[英]How to configure field annotated with @Mock in Spring Boot Unit test
[英]How to mock JWT authentication in a Spring Boot Unit Test?
我已按照此示例將使用 Auth0 的 JWT 身份驗證添加到我的 Spring Boot REST API。
現在,正如預期的那樣,我之前工作的Controller
單元測試給出了401 Unauthorized
而不是200 OK
的響應代碼,因為我在測試中沒有通過任何 JWT。
我如何模擬 REST Controller 測試的JWT/Authentication
部分?
@AutoConfigureMockMvc
public class UserRoundsControllerTest extends AbstractUnitTests {
private static String STUB_USER_ID = "user3";
private static String STUB_ROUND_ID = "7e3b270222252b2dadd547fb";
@Autowired
private MockMvc mockMvc;
private Round round;
private ObjectId objectId;
@BeforeEach
public void setUp() {
initMocks(this);
round = Mocks.roundOne();
objectId = Mocks.objectId();
}
@Test
public void shouldGetAllRoundsByUserId() throws Exception {
// setup
given(userRoundService.getAllRoundsByUserId(STUB_USER_ID)).willReturn(
Collections.singletonList(round));
// mock the rounds/userId request
RequestBuilder requestBuilder = Requests.getAllRoundsByUserId(STUB_USER_ID);
// perform the requests
MockHttpServletResponse response = mockMvc.perform(requestBuilder)
.andReturn()
.getResponse();
// asserts
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
}
//other tests
}
public class Requests {
private Requests() {}
public static RequestBuilder getAllRoundsByUserId(String userId) {
return MockMvcRequestBuilders
.get("/users/" + userId + "/rounds/")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON);
}
}
/**
* Configures our application with Spring Security to restrict access to our API endpoints.
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${auth0.audience}")
private String audience;
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
@Override
public void configure(HttpSecurity http) throws Exception {
/*
This is where we configure the security required for our endpoints and setup our app to serve as
an OAuth2 Resource Server, using JWT validation.
*/
http.cors().and().csrf().disable().sessionManagement().
sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
.mvcMatchers(HttpMethod.GET, "/users/**").authenticated()
.mvcMatchers(HttpMethod.POST, "/users/**").authenticated()
.mvcMatchers(HttpMethod.DELETE, "/users/**").authenticated()
.mvcMatchers(HttpMethod.PUT, "/users/**").authenticated()
.and()
.oauth2ResourceServer().jwt();
}
@Bean
JwtDecoder jwtDecoder() {
/*
By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
indeed intended for our app. Adding our own validator is easy to do:
*/
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer,
audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@ExtendWith(SpringExtension.class)
@SpringBootTest(
classes = PokerStatApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public abstract class AbstractUnitTests {
// mock objects etc
}
如果我正確理解您的情況,則有一種解決方案。
在大多數情況下,如果令牌存在於請求標頭中, JwtDecoder
bean 會執行令牌解析和驗證。
您的配置中的示例:
@Bean
JwtDecoder jwtDecoder() {
/*
By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
indeed intended for our app. Adding our own validator is easy to do:
*/
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
因此,對於測試,您需要添加此 bean 的存根並在 spring 上下文中替換此 bean,您需要使用它進行測試配置。
可能是這樣的:
@TestConfiguration
public class TestSecurityConfig {
static final String AUTH0_TOKEN = "token";
static final String SUB = "sub";
static final String AUTH0ID = "sms|12345678";
@Bean
public JwtDecoder jwtDecoder() {
// This anonymous class needs for the possibility of using SpyBean in test methods
// Lambda cannot be a spy with spring @SpyBean annotation
return new JwtDecoder() {
@Override
public Jwt decode(String token) {
return jwt();
}
};
}
public Jwt jwt() {
// This is a place to add general and maybe custom claims which should be available after parsing token in the live system
Map<String, Object> claims = Map.of(
SUB, USER_AUTH0ID
);
//This is an object that represents contents of jwt token after parsing
return new Jwt(
AUTH0_TOKEN,
Instant.now(),
Instant.now().plusSeconds(30),
Map.of("alg", "none"),
claims
);
}
}
要在測試中使用此配置,只需選擇此測試安全配置:
@SpringBootTest(classes = TestSecurityConfig.class)
同樣在測試請求中應該是授權 header 與像Bearer.. something
這樣的令牌。
這是有關您的配置的示例:
public static RequestBuilder getAllRoundsByUserId(String userId) {
return MockMvcRequestBuilders
.get("/users/" + userId + "/rounds/")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer token"))
.contentType(MediaType.APPLICATION_JSON);
}
對我來說,我做得很簡單。
我不想實際檢查 JWT 令牌,這也可以被嘲笑。
看看這個安全配置。
@Override
public void configure(HttpSecurity http) throws Exception {
//@formatter:off
http
.cors()
.and()
.authorizeRequests()
.antMatchers("/api/v1/orders/**")
.authenticated()
.and()
.authorizeRequests()
.anyRequest()
.denyAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer()
.jwt();
然后在我的測試中,我利用了兩件事
SecurityMockMvcRequestPostProcessors
在請求中模擬 JWT。 這在以下依賴項中可用 <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
這是它是如何完成的。
@SpringBootTest
@AutoConfigureMockMvc
public class OrderApiControllerIT {
@Autowired
protected MockMvc mockMvc;
@MockBean
private JwtDecoder jwtDecoder;
@Test
void testEndpoint() {
MvcResult mvcResult = mockMvc.perform(post("/api/v1/orders")
.with(SecurityMockMvcRequestPostProcessors.jwt())
.content(jsonString)
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andReturn();
}
就是這樣,它應該工作。
對於像我這樣的其他人,在從看起來像 gazillion StackOverlow 的關於如何執行此操作的答案中收集信息后,這里是最終對我有用的摘要(使用 Kotlin 語法,但它也適用於 Java):
第 1 步 - 定義要在測試中使用的自定義 JWT 解碼器
注意JwtClaimNames.SUB
條目 - 這是最終可通過authentication.getName()
字段訪問的用戶名。
val jwtDecoder = JwtDecoder {
Jwt(
"token",
Instant.now(),
Instant.MAX,
mapOf(
"alg" to "none"
),
mapOf(
JwtClaimNames.SUB to "testUser"
)
)
}
第 2 步 - 定義 TestConfiguration
這個 class 進入您的test
文件夾。 我們這樣做是為了用一個始終將用戶視為經過身份驗證的存根替換真正的實現。
請注意,我們還沒有完成,請檢查第 3 步。
@TestConfiguration
class TestAppConfiguration {
@Bean // important
fun jwtDecoder() {
// Initialize JWT decoder as described in step 1
// ...
return jwtDecoder
}
}
第 3 步 - 更新您的主要配置以避免 bean 沖突
如果不進行此更改,您的測試和生產 bean 將發生沖突,從而導致沖突。 添加此行會延遲 bean 的分辨率,並讓 Spring 將測試 bean 優先於生產 bean。
但是,有一個警告,因為此更改有效地刪除了 JwtDecoder 實例的生產構建中的 bean 沖突保護。
@Configuration
class AppConfiguration {
@Bean
@ConditionalOnMissingBean // important
fun jwtDecoder() {
// Provide decoder as you would usually do
}
}
第 4 步 - 在您的測試中導入 TestAppConfiguration
這可以確保您的測試實際上將 TestConfiguration 考慮在內。
@SpringBootTest
@Import(TestAppConfiguration::class)
class MyTest {
// Your tests
}
第 5 步 - 將 @WithMockUser 注釋添加到您的測試中
您實際上不需要為注釋提供任何 arguments。
@Test
@WithMockUser
fun myTest() {
// Test body
}
第 6 步 - 在測試期間提供身份驗證 header
mockMvc
.perform(
post("/endpointUnderTest")
.header(HttpHeaders.AUTHORIZATION, "Bearer token") // important
)
.andExpect(status().isOk)
SecurityConfig bean 可以有條件地加載為,
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Profile("!test")
public WebSecurityConfigurerAdapter securityEnabled() {
return new WebSecurityConfigurerAdapter() {
@Override
protected void configure(HttpSecurity http) throws Exception {
// your code goes here
}
};
}
@Bean
@Profile("test")
public WebSecurityConfigurerAdapter securityDisabled() {
return new WebSecurityConfigurerAdapter() {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
};
}
}
所以在測試配置文件的情況下這個bean不會被初始化。 這意味着現在安全性被禁用,所有端點都可以在沒有任何授權的情況下訪問 header。
現在“測試”配置文件需要在運行測試的情況下處於活動狀態,這可以這樣做,
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@WebMvcTest(UserRoundsController.class)
public class UserRoundsControllerTest extends AbstractUnitTests {
// your code goes here
}
現在這個測試將使用配置文件“test”運行。 此外,如果您想擁有與此測試相關的任何屬性,可以將其放在 src/test/resources/application-test.properties 下。
希望這可以幫助。 否則請告訴我。
更新:基本想法是禁用測試配置文件的安全性。 在之前的代碼中,即使在配置了特定於配置文件的 bean 之后,默認安全性也已啟用。
您可以獲得 Bearer 令牌並將其作為 HTTP Header 傳遞。 以下是測試方法的示例片段供您參考,
@Test
public void existentUserCanGetTokenAndAuthentication() throws Exception {
String username = "existentuser";
String password = "password";
String body = "{\"username\":\"" + username + "\", \"password\":\"
+ password + "\"}";
MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/token")
.content(body))
.andExpect(status().isOk()).andReturn();
String response = result.getResponse().getContentAsString();
response = response.replace("{\"access_token\": \"", "");
String token = response.replace("\"}", "");
mvc.perform(MockMvcRequestBuilders.get("/users/" + userId + "/rounds")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
嘗試使用@WithMockUser
@Test
@WithMockUser(username="ahmed",roles={"ADMIN"})
public void shouldGetAllRoundsByUserId() throws Exception {
在 test/resources 中創建application.properties (它將覆蓋 main 但僅用於測試階段)
通過指定關閉安全性:
security.ignored=/**
security.basic.enable= false
spring.autoconfigure.exclude= org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
我希望這可以幫助別人。 對我來說 mocking 在要模擬身份驗證的測試中使用 @WithUserDetails 解決了這個問題。 我還使用 JWT 進行身份驗證。
我正在使用安全上下文中的JwtAuthenticationToken
。 @WithMockUser
注釋正在創建一個基於用戶名的身份驗證令牌。
我自己編寫了@WithMockJwt
的實現:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithMockJwtSecurityContextFactory.class)
public @interface WithMockJwt {
long value() default 1L;
String[] roles() default {};
String email() default "ex@example.org";
}
以及相關工廠:
public class WithMockJwtSecurityContextFactory implements WithSecurityContextFactory<WithMockJwt> {
@Override
public SecurityContext createSecurityContext(WithMockJwt annotation) {
val jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", annotation.value())
.claim("user", Map.of("email", annotation.email()))
.build();
val authorities = AuthorityUtils.createAuthorityList(annotation.roles());
val token = new JwtAuthenticationToken(jwt, authorities);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(token);
return context;
}
}
現在我可以用以下方式注釋測試:
@Test
@WithMockJwt
void test() {
...omissis...
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.