[英]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
部分?
@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
}
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然后在我的测试中,我利用了两件事
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.