簡體   English   中英

如何在 Spring 引導單元測試中模擬 JWT 身份驗證?

[英]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部分?

單元測試 class

@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
}

請求 class(上面使用)

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);
    }
}

Spring 安全配置

/**
 * 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;
    }
}

抽象單元測試 class

@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();

然后在我的測試中,我利用了兩件事

  • 為 jwtDecoder 提供一個模擬 bean
  • 使用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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM