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

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

我有一个控制器,它会向用户提供 403 响应,除非他们使用 JWT 令牌进行身份验证,该令牌通过授权 header 作为 Bearer 令牌传递。 我正在寻找有关如何使用 Mockito 进行测试的资源,但我并不是很成功,因为他们中的大多数人告诉我使用 @WithMockUser 注释,我知道这是为了 Spring 安全是的,但是不包括 JWT 令牌的模拟。我尝试模拟一些 objects,例如 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());
}

这是我的控制器:

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

}
}

这是我的过滤器:

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

    }


}

}

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

这是整个 Github 分支。

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

谢谢。

更新

为清楚起见,如果我对有效令牌进行硬编码,我会得到一个 200 状态代码,但我的测试仍然会失败,并且不会返回任何内容,而在 JWT 和 Spring 安全性之前,它们已经通过了。

我们刚刚解决了这个问题(接受另一个答案是一个更优雅的解决方案)。

第一个更简单的选项:

为控制器测试禁用过滤器身份验证classes:

@AutoConfigureMockMvc(addFilters = false)
class CategoryCommandControllerTest {

然后您或许可以单独测试 jwt 授权。

第二个也许更好的选择:

从 WebSecurity class 中的配置方法中删除多余的部分,以仅此结束。

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

    http.csrf().disable();
}

然后在 JwtRequestFilter class 下添加一个 return 当在这个 if 块的 else 部分捕获到 403 时。

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

然后将 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
private JwtUtil jwtUtil;

这使得 JwtRequestFilter 在

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

因为 username 将始终 return 来自模拟 bean 的 null。

要使用实际的 JwtUtils 添加 includeFilters 以将其包含在 spring 上下文中, 然后我们还需要模拟 myUserDetailsService.loadUserByUsernameJwtRequestFilter 中使用。之后测试将通过。 请参阅下面代码中的注释以了解更改。

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