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

Posted

技术标签:

【中文标题】如何模拟 JWT 令牌以将其与 Mockito 和 Spring Boot 一起使用【英文标题】:How to mock JWT token to use it with Mockito and Spring Boot 【发布时间】:2022-01-12 16:09:15 【问题描述】:

我有一个控制器,它给用户一个 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());

这里是我的控制器:

@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 类:

@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 安全性之前它们通过了。

【问题讨论】:

您有一个可以生成 JWT 的实用程序函数。为什么不在测试中使用它来生成新的令牌并将其放入标头中?我什至可能会为所有使用有效令牌的测试生成一次令牌。 即使我硬编码一个有效的令牌,它也不起作用。它会给我一个 200 状态码,是的,但测试将失败,没有任何结果可以打印。不知道为什么。 你能创建一个可以重现你的问题的分支吗? github.com/francislainy/adverts-backend/tree/dev_jwt 【参考方案1】:

主要问题是使用

@MockBean
private JwtUtil jwtUtil;

这使得 JwtRequestFilter 在中执行错误

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

username 将始终从模拟 bean 返回 null。

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

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

【讨论】:

感谢您的回答和时间调查此问题。非常感谢。【参考方案2】:

我们刚刚解决了这个问题(接受其他答案作为更优雅的解决方案)。

第一个更简单的选择:

禁用控制器测试类的过滤器身份验证:

@AutoConfigureMockMvc(addFilters = false)
class CategoryCommandControllerTest 

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

第二个也许更好的选择:

从 WebSecurity 类中的 configure 方法中删除多余的部分,最终只得到这个。

@Override
protected void configure(HttpSecurity http) throws Exception 

    http.csrf().disable();

然后在 JwtRequestFilter 类下添加一个返回,当在这个 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);


【讨论】:

以上是关于如何模拟 JWT 令牌以将其与 Mockito 和 Spring Boot 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

如何用 mockito 测试这个类?

如何从标头中正确获取 jwt 令牌并将其与 express 一起使用

如何使用标头中的用户 JWT 令牌转发授权并将其与 cookie 中的 JWT 令牌进行比较以验证用户?

如何配置 Git 以将其与 Dropbox 一起使用?

如何保护刷新令牌?

如何加载 IP 范围列表以将其与 $allowedIps 脚本一起使用?