如何模拟 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 一起使用的主要内容,如果未能解决你的问题,请参考以下文章
如何从标头中正确获取 jwt 令牌并将其与 express 一起使用