简体   繁体   English

如何在 Spring 引导单元测试中模拟 JWT 身份验证?

[英]How to mock JWT authentication in a Spring Boot Unit Test?

I have added JWT Authentication using Auth0 to my Spring Boot REST API following this example .我已按照此示例将使用 Auth0 的 JWT 身份验证添加到我的 Spring Boot REST API。

Now, as expected, my previously working Controller unit tests give a response code of 401 Unauthorized rather than 200 OK as I am not passing any JWT in the tests.现在,正如预期的那样,我之前工作的Controller单元测试给出了401 Unauthorized而不是200 OK的响应代码,因为我在测试中没有通过任何 JWT。

How can I mock the JWT/Authentication part of my REST Controller tests?我如何模拟 REST Controller 测试的JWT/Authentication部分?

Unit test class单元测试 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
}

Requests class (used above)请求 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 Security Config 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;
    }
}

Abstract Unit test class抽象单元测试 class

@ExtendWith(SpringExtension.class)
@SpringBootTest(
        classes = PokerStatApplication.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public abstract class AbstractUnitTests {
    // mock objects etc
}

If I understand correctly your case there is one of the solutions.如果我正确理解您的情况,则有一种解决方案。

In most cases, JwtDecoder bean performs token parsing and validation if the token exists in the request headers.在大多数情况下,如果令牌存在于请求标头中, JwtDecoder bean 会执行令牌解析和验证。

Example from your configuration:您的配置中的示例:

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

So for the tests, you need to add stub of this bean and also for replacing this bean in spring context, you need the test configuration with it.因此,对于测试,您需要添加此 bean 的存根并在 spring 上下文中替换此 bean,您需要使用它进行测试配置。

It can be some things like this:可能是这样的:

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

}

For using this configuration in tests just pick up this test security config:要在测试中使用此配置,只需选择此测试安全配置:

@SpringBootTest(classes = TestSecurityConfig.class)

Also in the test request should be authorization header with a token like Bearer.. something .同样在测试请求中应该是授权 header 与像Bearer.. something这样的令牌。

Here is an example regarding your configuration:这是有关您的配置的示例:

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

For me, I made it pretty simple.对我来说,我做得很简单。

I don't want to actually check for the JWT token, this can also be mocked.我不想实际检查 JWT 令牌,这也可以被嘲笑。

Have a look at this security config.看看这个安全配置。

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

Then in my test, I make use of two thing然后在我的测试中,我利用了两件事

  • Provide a mock bean for the jwtDecoder为 jwtDecoder 提供一个模拟 bean
  • Use the SecurityMockMvcRequestPostProcessors to mock the JWT in the request.使用SecurityMockMvcRequestPostProcessors在请求中模拟 JWT。 This is available in the following dependency这在以下依赖项中可用
         <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

And Here is how it's done.这是它是如何完成的。

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

}

That's it and it should work.就是这样,它应该工作。

For others like me, who after gathering information from what seems like a gazillion StackOverlow answers on how to do this, here is the summary of what ultimately worked for me (using Kotlin syntax, but it is applicable to Java as well):对于像我这样的其他人,在从看起来像 gazillion StackOverlow 的关于如何执行此操作的答案中收集信息后,这里是最终对我有用的摘要(使用 Kotlin 语法,但它也适用于 Java):

Step 1 - Define a custom JWT decoder to be used in tests第 1 步 - 定义要在测试中使用的自定义 JWT 解码器

Notice the JwtClaimNames.SUB entry - this is the user name which will ultimately be accessible via authentication.getName() field.注意JwtClaimNames.SUB条目 - 这是最终可通过authentication.getName()字段访问的用户名。

val jwtDecoder = JwtDecoder {
        Jwt(
                "token",
                Instant.now(),
                Instant.MAX,
                mapOf(
                        "alg" to "none"
                ),
                mapOf(
                        JwtClaimNames.SUB to "testUser"
                )
        )
}

Step 2 - Define a TestConfiguration第 2 步 - 定义 TestConfiguration

This class goes in your test folder.这个 class 进入您的test文件夹。 We do this to replace real implementation with a stub one which always treats the user as authenticated.我们这样做是为了用一个始终将用户视为经过身份验证的存根替换真正的实现。

Note that we are not done yet, check Step 3 as well.请注意,我们还没有完成,请检查第 3 步。

@TestConfiguration
class TestAppConfiguration {

    @Bean // important
    fun jwtDecoder() {
        // Initialize JWT decoder as described in step 1
        // ...

        return jwtDecoder
    }

}

Step 3 - Update your primary configuration to avoid bean conflict第 3 步 - 更新您的主要配置以避免 bean 冲突

Without this change your test and production beans would clash, resulting in a conflict.如果不进行此更改,您的测试和生产 bean 将发生冲突,从而导致冲突。 Adding this line delays the resolution of the bean and lets Spring prioritise test bean over production one.添加此行会延迟 bean 的分辨率,并让 Spring 将测试 bean 优先于生产 bean。

There is a caveat, however, as this change effectively removes bean conflict protection in production builds for JwtDecoder instances.但是,有一个警告,因为此更改有效地删除了 JwtDecoder 实例的生产构建中的 bean 冲突保护。

@Configuration
class AppConfiguration {

    @Bean
    @ConditionalOnMissingBean // important
    fun jwtDecoder() {
        // Provide decoder as you would usually do
    }

}

Step 4 - Import TestAppConfiguration in your test第 4 步 - 在您的测试中导入 TestAppConfiguration

This makes sure that your test actually takes TestConfiguration into account.这可以确保您的测试实际上将 TestConfiguration 考虑在内。

@SpringBootTest
@Import(TestAppConfiguration::class)
class MyTest {

    // Your tests

}

Step 5 - Add @WithMockUser annotation to your test第 5 步 - 将 @WithMockUser 注释添加到您的测试中

You do not really need to provide any arguments to the annotation.您实际上不需要为注释提供任何 arguments。

@Test
@WithMockUser
fun myTest() {
    // Test body
}

Step 6 - Provide Authentication header during the test第 6 步 - 在测试期间提供身份验证 header

mockMvc
    .perform(
        post("/endpointUnderTest")
            .header(HttpHeaders.AUTHORIZATION, "Bearer token") // important
    )
    .andExpect(status().isOk)

SecurityConfig bean can be loaded conditionally as, 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();
      }
    };
  }

}

So this bean won't be initialized in case of test profile.所以在测试配置文件的情况下这个bean不会被初始化。 It means now security is disabled and all endpoints are accessible without any authorization header.这意味着现在安全性被禁用,所有端点都可以在没有任何授权的情况下访问 header。

Now "test" profile needs to be active in case of running the tests, this can be done as,现在“测试”配置文件需要在运行测试的情况下处于活动状态,这可以这样做,

@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@WebMvcTest(UserRoundsController.class)
public class UserRoundsControllerTest extends AbstractUnitTests {

// your code goes here

}

Now this test is going to run with profile "test".现在这个测试将使用配置文件“test”运行。 Further if you want to have any properties related to this test, that can be put under src/test/resources/application-test.properties.此外,如果您想拥有与此测试相关的任何属性,可以将其放在 src/test/resources/application-test.properties 下。

Hope this helps.希望这可以帮助。 please let me know otherwise.否则请告诉我。

Update: Basic idea is to disable security for test profile.更新:基本想法是禁用测试配置文件的安全性。 In previous code, even after having profile specific bean, default security was getting enabled.在之前的代码中,即使在配置了特定于配置文件的 bean 之后,默认安全性也已启用。

You can get the Bearer token and pass it on as a HTTP Header.您可以获得 Bearer 令牌并将其作为 HTTP Header 传递。 Below is a sample snippet of the Test Method for your reference,以下是测试方法的示例片段供您参考,

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

try with @WithMockUser尝试使用@WithMockUser

    @Test
    @WithMockUser(username="ahmed",roles={"ADMIN"})
    public void shouldGetAllRoundsByUserId() throws Exception {

create application.properties in test/resources (it will override main but for test stage only)在 test/resources 中创建application.properties (它将覆盖 main 但仅用于测试阶段)

turnoff security by specifyig:通过指定关闭安全性:

security.ignored=/**
security.basic.enable= false
spring.autoconfigure.exclude= org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

I hope this helps someone.我希望这可以帮助别人。 For me mocking using @WithUserDetails on the test you want to mock the authentication solved the problem.对我来说 mocking 在要模拟身份验证的测试中使用 @WithUserDetails 解决了这个问题。 I'm also using JWT for authentication.我还使用 JWT 进行身份验证。

I am using the JwtAuthenticationToken from the Security Context.我正在使用安全上下文中的JwtAuthenticationToken The @WithMockUser annotation is creating a Username-based Authentication Token. @WithMockUser注释正在创建一个基于用户名的身份验证令牌。

I wrote my own implementation of @WithMockJwt :我自己编写了@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";

}

And the related factory:以及相关工厂:

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;

    }
}

And now I can annotate test with:现在我可以用以下方式注释测试:

    @Test
    @WithMockJwt
    void test() {

     ...omissis...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何在 Spring Boot 单元测试中配置用@Mock 注释的字段 - How to configure field annotated with @Mock in Spring Boot Unit test 使用 SpringSecurity 和 Mock 进行 Spring Boot 单元测试 - Spring Boot unit test with SpringSecurity and Mock Spring Boot Rest Api的单元测试模拟 - Unit Test Mock for Spring Boot Rest Api 在 Spring Boot 中使用模拟单元测试接口实现 - unit test a interface implementation with mock in Spring Boot Spring 引导 - 如何对 @Service class 方法在使用 JWT 和自定义声明时使用 @PreAuthorize 进行单元测试 - Spring Boot - How to unit test @Service class methods that use @PreAuthorize when using JWT with custom claims Spring Boot Java 无法模拟 JPARepository 进行单元测试 - Spring Boot Java Cannot Mock JPARepository for Unit Test model 映射器模拟在 Z2A2D595E6ED9A0B24F027F2B63B134D 引导单元中返回 null object - model mapper mock returns null object in spring boot unit test 测试单元 Spring 启动:无法注册模拟 bean - Test Unit Spring boot: Unable to register mock bean 如何在Spring中用Mock编写CommonsMultipartFile的单元测试 - How to write unit test for CommonsMultipartFile with Mock in Spring 如何对OAUTH Spring Boot进行模拟测试 - How to make a Mock Test for OAUTH Spring Boot
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM