Issue
I'm adding an admin filter to a specific URL like this
@Bean
public FilterRegistrationBean<AdminFilter> adminFilterRegistrationBean() {
FilterRegistrationBean<AdminFilter> registrationBean = new FilterRegistrationBean<>();
AdminFilter adminFilter = new AdminFilter();
registrationBean.setFilter(adminFilter);
registrationBean.addUrlPatterns("/api/user/activate");
registrationBean.addUrlPatterns("/api/user/deactivate");
registrationBean.setOrder(Integer.MAX_VALUE);
return registrationBean;
}
While I'm testing it with postman or in browser, the filter is applied correctly, only applied to those URL pattern.
But, when I write test for it, somehow the filter is applied to another URL too.
this.mockMvc.perform(
get("/api/issue/").header("Authorization", defaultToken)
).andDo(print()).andExpect(status().isOk())
.andExpect(content().json("{}"));
This code return an error with code "403", on the log it says because the user is not an admin, which means the admin filter applied to "/api/issue/" URL on the mock mvc request.
I'm using @AutoConfigureMockMvc
with @Autowired
to instantiate the mockMVC.
anyone know why it's happening?
Full code of the admin filter:
@Component
public class AdminFilter extends GenericFilterBean {
UserService userService;
@Override
public void doFilter(
ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain
) throws IOException, ServletException {
if (userService == null){
ServletContext servletContext = servletRequest.getServletContext();
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
userService = webApplicationContext.getBean(UserService.class);
}
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
UUID userId = UUID.fromString((String)httpRequest.getAttribute("userId"));
User user = userService.fetchUserById(userId);
if (!user.getIsAdmin()) {
httpResponse.sendError(HttpStatus.FORBIDDEN.value(), "User is not an admin");
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
Full code of the test file:
@SpringBootTest()
@AutoConfigureMockMvc
@Transactional
public class RepositoryIntegrationTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private RepositoryRepository repositoryRepository;
@Autowired
private UserRepository userRepository;
private String defaultToken;
private String otherToken;
@BeforeEach
void init() {
User defaultUser = userRepository.save(new User("username", "[email protected]", "password"));
System.out.println(defaultUser);
User otherUser = userRepository.save(new User("other", "[email protected]", "password"));
defaultToken = "Bearer " + generateJWTToken(defaultUser);
otherToken = "Bearer " + generateJWTToken(otherUser);
}
private String generateJWTToken(User user) {
long timestamp = System.currentTimeMillis();
return Jwts.builder().signWith(SignatureAlgorithm.HS256, Constants.API_SECRET_KEY)
.setIssuedAt(new Date(timestamp))
.setExpiration(new Date(timestamp + Constants.TOKEN_VALIDITY))
.claim("userId", user.getId())
.compact();
}
@Test
public void shouldReturnAllRepositoriesAvailableToUser() throws Exception {
this.mockMvc.perform(
get("/api/issue/").header("Authorization", defaultToken)
).andExpect(status().isOk())
.andExpect(content().json("{}"));
}
}
Solution
Your AdminFilter
is being registered twice. Once through the FilterRegistrationBean
and once due to the fact that it is an @Component
and thus detected by component scanning.
To fix do one of 2 things
- Remove
@Component
- Re-use the automatically created instance for the
FilterRegistrationBean
.
Removing @Component
is easy enough, just remove it from the class.
For option 2 you can inject the automatically configured filter into the FilterRegistrationBean
configuration method, instead of creating it yourself.
@Bean
public FilterRegistrationBean<AdminFilter> adminFilterRegistrationBean(AdminFilter adminFilter) {
FilterRegistrationBean<AdminFilter> registrationBean = new FilterRegistrationBean<>(adminFilter);
registrationBean.addUrlPatterns("/api/user/activate");
registrationBean.addUrlPatterns("/api/user/deactivate");
registrationBean.setOrder(Integer.MAX_VALUE);
return registrationBean;
}
An added advantage of this is that you can use autowiring to set up dependencies instead of doing lookups in the init
method. I would also suggest using the OncePerRequestFilter
. This would clean up your filter considerably.
@Component
public class AdminFilter extends OncePerRequestFilter {
private final UserService userService;
public AdminFilter(UserService userService) {
this.userService=userService;
}
@Override
protected void doFilter(HttpServletRequest httpRequest, HttpServletResponse httpResponse, FilterChain filterChain) throws IOException, ServletException {
UUID userId = UUID.fromString((String)httpRequest.getAttribute("userId"));
User user = userService.fetchUserById(userId);
if (!user.getIsAdmin()) {
httpResponse.sendError(HttpStatus.FORBIDDEN.value(), "User is not an admin");
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
Answered By - M. Deinum
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)