如何模拟 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.loadUserByUsername
在 JwtRequestFilter
中使用。之后测试将通过。
请参阅下面代码中的注释以了解更改。
@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());
}
...
}
我有一个控制器,它会向用户提供 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.loadUserByUsername
在 JwtRequestFilter
中使用。之后测试将通过。
请参阅下面代码中的注释以了解更改。
@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());
}
...
}