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

Fra*_*pos 6 java mockito jwt spring-boot

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

这是我的控制器:

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

}
}
Run Code Online (Sandbox Code Playgroud)

这是我的过滤器:

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

    }


}

}
Run Code Online (Sandbox Code Playgroud)

和 JwtUtil 类:

@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));
}
}
Run Code Online (Sandbox Code Playgroud)

这是整个 Github 分支。

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

谢谢。

更新

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

在此输入图像描述

sam*_*cde 2

主要问题是使用

@MockBean
private JwtUtil jwtUtil;
Run Code Online (Sandbox Code Playgroud)

这使得 JwtRequestFilter 执行错误

@MockBean
private JwtUtil jwtUtil;
Run Code Online (Sandbox Code Playgroud)

因为username模拟 bean 总是返回 null。

要使用实际的JwtUtils AddincludeFilters将其包含在 spring 上下文中,那么我们还需要myUserDetailsService.loadUserByUsername模拟JwtRequestFilter. 之后测试就会通过。请参阅下面代码中的注释以了解更改。

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