简体   繁体   English

如何模拟 JWT 令牌以将其与 Mockito 和 Spring 引导一起使用

[英]How to mock JWT token to use it with Mockito and Spring Boot

I have a controller which gives the user a 403 response unless they are authenticated with a JWT token which is passed as a Bearer token via the authorization header.我有一个 controller 给用户一个 403 响应,除非他们使用 JWT 令牌进行身份验证,该令牌通过授权 header 作为承载令牌传递。 I'm looking for resources on how to test this with Mockito but I'm not very successful so far as most of them tell me to use the @WithMockUser annotation, which I understand is for Spring security yes, but does not include the mocking for a JWT token.我正在寻找有关如何使用 Mockito 进行测试的资源,但到目前为止,我并不是很成功,因为他们中的大多数人告诉我使用 @WithMockUser 注释,据我所知,这是针对 Spring 安全性的,但不包括 ZD1817BD0850230BA0288对于 JWT 令牌。 I've tried to mock a few objects such as the UserDetailsClass and the JwtFilter and even hardcoding the bearer token but I think there should be more to it.我尝试模拟一些对象,例如 UserDetailsClass 和 JwtFilter ,甚至对不记名令牌进行硬编码,但我认为应该有更多。

@MockBean
private CategoryCommandService categoryCommandService;

@Autowired
private MockMvc mockMvc;

@MockBean
private MyUserDetailsService myUserDetailsService;

@MockBean
private CategoryRepository categoryRepository;

@MockBean
private JwtUtil jwtUtil;

@Autowired
private JwtRequestFilter filter;


@Test
void testCreateCategory() throws Exception {

    CategoryCreateDto categoryCreateDto = new CategoryCreateDto("category");
    CategoryCreateDto categoryCreateResponseDto = new CategoryCreateDto(UUID.fromString("2da4002a-31c5-4cc7-9b92-cbf0db998c41"), "category");

    String jsonCreate = asJsonString(categoryCreateDto);
    String jsonResponse = asJsonString(categoryCreateResponseDto);

    RequestBuilder request = MockMvcRequestBuilders
            .post("/api/adverts/category")
            .content(jsonCreate)
            .header("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmb29AZW1haWwuY29tIiwiZXhwIjoxNjM4ODU1MzA1LCJpYXQiOjE2Mzg4MTkzMDV9.q4FWV7yVDAs_DREiF524VZ-udnqwV81GEOgdCj6QQAs")
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .accept(MediaType.APPLICATION_JSON);
    mockMvc.perform(request).andReturn();

    when(categoryCommandService.createCategory(categoryCreateDto)).thenReturn(
            categoryCreateResponseDto);

    MvcResult mvcResult = mockMvc.perform(request)
            .andExpect(status().is2xxSuccessful())
            .andExpect(content().json(jsonResponse, true))
            .andExpect(jsonPath("$.id").value("2da4002a-31c5-4cc7-9b92-cbf0db998c41"))
            .andExpect(jsonPath("$.title").value("category"))
            .andReturn();

    logger.info(mvcResult.getResponse().getContentAsString());
}

Here my controller:这是我的 controller:

@CrossOrigin
@RequestMapping("/api/adverts/category")
@RestController
public class CategoryCommandController {

@Autowired
private CategoryCommandService categoryCommandService;

@Autowired
private CategoryRepository categoryRepository;

@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> createCategory(@RequestBody CategoryCreateDto categoryCreateDto) {

    if (categoryCreateDto.getTitle() != null) {
        return new ResponseEntity<>(categoryCommandService.createCategory(categoryCreateDto), HttpStatus.CREATED);
    }
    else {
        return new ResponseEntity<>(new FeedbackMessage("Missing title"), HttpStatus.BAD_REQUEST);
    }

}
}

And here my filter:这是我的过滤器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static com.example.adverts.SecurityConstants.SIGN_UP_URL;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

@Autowired
private MyUserDetailsService userDetailsService;

@Autowired
private JwtUtil jwtUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws ServletException, IOException {

    String path = request.getRequestURI();
    if (path.equals(SIGN_UP_URL)) {
        chain.doFilter(request, response);
        return;
    }

    final String authorizationHeader = request.getHeader("Authorization");

    String username = null;
    String jwt = null;

    if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
        jwt = authorizationHeader.substring(7);
        username = jwtUtil.extractUsername(jwt);
    } else {

        response.setStatus(HttpStatus.FORBIDDEN.value());
    }

    if (username != null) {

        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

        if (jwtUtil.validateToken(jwt, userDetails)) {

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            usernamePasswordAuthenticationToken
                    .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }

        chain.doFilter(request, response);

    }


}

}

And JwtUtil class:和 JwtUtil class:

@Service
public class JwtUtil {

private String SECRET_KEY = "secret";

public String extractUsername(String token) {
    return extractClaim(token, Claims::getSubject);
}

public Date extractExpiration(String token) {
    return extractClaim(token, Claims::getExpiration);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = extractAllClaims(token);
    return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
    return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}

private Boolean isTokenExpired(String token) {
    return extractExpiration(token).before(new Date());
}

public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return createToken(claims, userDetails.getUsername());
}

private String createToken(Map<String, Object> claims, String subject) {

    return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
}

public Boolean validateToken(String token, UserDetails userDetails) {
    final String username = extractUsername(token);
    return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

Here is the whole Github branch.这是整个 Github 分支。

https://github.com/francislainy/adverts-backend/tree/dev_jwthttps://github.com/francislainy/adverts-backend/tree/dev_jwt

Thank you.谢谢你。

UPDATE更新

For clarity if I hardcode a valid token I get a 200 status code but my tests will still fail with nothing returned for content whereas before JWT and Spring security they were passing.为清楚起见,如果我硬编码一个有效的令牌,我会得到一个 200 状态代码,但我的测试仍然会失败,内容不会返回任何内容,而在 JWT 和 Spring 安全之前他们通过了。

在此处输入图像描述

The main problem is using主要问题是使用

@MockBean
private JwtUtil jwtUtil;

Which make JwtRequestFilter perform wrongly in这使得 JwtRequestFilter 在

    if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
        jwt = authorizationHeader.substring(7);
        username = jwtUtil.extractUsername(jwt);
    }

As username will always return null from the mock bean.因为username名将始终从模拟 bean 返回 null。

To use the actual JwtUtils Add includeFilters to include it in spring context, then we also need to mock myUserDetailsService.loadUserByUsername used in JwtRequestFilter .要使用实际的JwtUtils添加includeFilters以将其包含在 spring 上下文中,那么我们还需要模拟myUserDetailsService.loadUserByUsername中使用的JwtRequestFilter After that the test will pass.之后,测试将通过。 Refer to comment inside below code for the changes.有关更改,请参阅下面代码中的注释。

@WebMvcTest(value = CategoryCommandController.class, includeFilters = {
        // to include JwtUtil in spring context
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = JwtUtil.class)})
class CategoryCommandControllerTest {

    Logger logger = LoggerFactory.getLogger(CategoryCommandController.class);

    @MockBean
    private CategoryCommandService categoryCommandService;

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MyUserDetailsService myUserDetailsService;

    @MockBean
    private CategoryRepository categoryRepository;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private JwtRequestFilter filter;

    //    @WithMockUser
    @Test
    void testCreateCategory() throws Exception {

        CategoryCreateDto categoryCreateDto = new CategoryCreateDto("category");
        CategoryCreateDto categoryCreateResponseDto = new CategoryCreateDto(UUID.fromString("2da4002a-31c5-4cc7-9b92-cbf0db998c41"), "category");

        String jsonCreate = asJsonString(categoryCreateDto);
        String jsonResponse = asJsonString(categoryCreateResponseDto);
        UserDetails dummy = new User("foo@email.com", "foo", new ArrayList<>());
        String jwtToken = jwtUtil.generateToken(dummy);
        RequestBuilder request = MockMvcRequestBuilders
                .post("/api/adverts/category")
                .content(jsonCreate)
                .header("Authorization", "Bearer " + jwtToken)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .accept(MediaType.APPLICATION_JSON);
// Below line is not used
//        mockMvc.perform(request).andReturn();

        //             Should be createCategory(eq(categoryCreateDto))?
        when(categoryCommandService.createCategory(categoryCreateDto)).thenReturn(
                categoryCreateResponseDto);
        // Mock Service method used in JwtRequestFilter
        when(myUserDetailsService.loadUserByUsername(eq("foo@email.com"))).thenReturn(dummy);
        MvcResult mvcResult = mockMvc.perform(request)
                .andExpect(status().is2xxSuccessful())
//                .andExpect(content().json(jsonResponse, true))
                .andExpect(jsonPath("$.id").value("2da4002a-31c5-4cc7-9b92-cbf0db998c41"))
//                .andExpect(jsonPath("$.title").value("category"))
                .andReturn();

        logger.info(mvcResult.getResponse().getContentAsString());
    }
    ...
}

We just fixed the issue (accepting the other answer for being a more elegant solution).我们刚刚解决了这个问题(接受其他答案作为更优雅的解决方案)。

1st and easier option:第一个更简单的选择:

Disable filter authentication for controller test classes:禁用 controller 测试类的过滤器身份验证:

@AutoConfigureMockMvc(addFilters = false)
class CategoryCommandControllerTest {

You can then perhaps test jwt authorization separately.然后,您也许可以单独测试 jwt 授权。

2nd and perhaps better option:第二个也许更好的选择:

Remove the extra pieces from the configure method within the WebSecurity class to end up with only this.从 WebSecurity class 中的配置方法中删除额外的部分,最终只得到这个。

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.csrf().disable();
}

Then under the JwtRequestFilter class add a return when a 403 is caught on the else part of this if block.然后在 JwtRequestFilter class 下添加一个返回,当在这个 if 块的 else 部分捕获 403 时。

 if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
        jwt = authorizationHeader.substring(7);
        username = jwtUtil.extractUsername(jwt);
    } else {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return;
    }

And move the doChain.filter piece outside of the other if block.并将 doChain.filter 块移到另一个 if 块之外。

  if (username != null) {

        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

        if (jwtUtil.validateToken(jwt, userDetails)) {

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            usernamePasswordAuthenticationToken
                    .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }

        // chain.doFilter(request, response);

    }

    chain.doFilter(request, response);

}

@MockBean JwtDecoder jwtDecoder; should be enough in association with either:应该足够与以下任何一个相关联:

Both in action:两者都在行动:

@WebMvcTest(GreetingController.class)
@Import(SampleApi.WebSecurityConfig.class)
class GreetingControllerAnnotatedTest {

    @MockBean JwtDecoder jwtDecoder;

    @Autowired
    MockMvc api;

    @Test
    @WithMockJwtAuth(authorities = "ROLE_AUTHORIZED_PERSONNEL", claims = @OpenIdClaims(sub = "Ch4mpy", preferredUsername = "Tonton Pirate"))
    void greetWithAnnotation() throws Exception {
        api.perform(get("/greet")).andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
    }

    @Test
    void greetWithPostProcessor() throws Exception {
        api.perform(get("/greet").with(SecurityMockMvcRequestPostProcessors.jwt()
                .authorities(List.of(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL"))).jwt(jwt -> {
                    jwt.subject("Ch4mpy");
                    jwt.claims(claims -> claims.put(StandardClaimNames.PREFERRED_USERNAME, "Tonton Pirate"));
                }))).andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
    }
}

Complete sample there 在那里完成样品

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

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM