Reputation: 1
I am developing rest api for forum in spring boot. I have GET mapping on /api/v1/forum/threads, which will return all the threads. But it returns 401 Unauthorized when I try to access it, even if I am authorized.
Thread Controller:
@RestController
@RequestMapping("/api/v1/forum/threads")
public class ThreadController {
@Autowired
private ThreadService threadService;
@GetMapping
public ResponseEntity<List<Thread>> getAllCategories(){
List<Thread> threads = threadService.findAll();
return ResponseEntity.ok(threads);
}
}
Thread service:
@Service
public class ThreadService {
@Autowired
private ThreadRepository threadRepository;
public Optional<Thread> findById(long id) {
return threadRepository.findById(id);
}
public Optional<List<Thread>> findByName(String keyword){
return threadRepository.findByNameContainingIgnoreCase(keyword);
}
public Optional<List<Thread>> findByCategory(Long id){
return threadRepository.findByCategoryId(id);
}
public List<Thread> findAll(){
return threadRepository.findAll();
}
public Optional<List<Thread>> findByContent(String keyword){
return threadRepository.findByContentContainingIgnoreCase(keyword);
}
public void createThread(Thread thread){
threadRepository.save(thread);
}
public void deleteThread(Long id){
threadRepository.deleteById(id);
}
}
Thread Repository:
@Repository
public interface ThreadRepository extends JpaRepository<Thread, Long> {
Optional<Thread> findById(Long id);
Optional<List<Thread>> findByNameContainingIgnoreCase(String title);
Optional<List<Thread>> findByCategoryId(Long categoryId);
Optional<List<Thread>> findByContentContainingIgnoreCase(String keyword);
}
Security config:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
UserDetailsService userDetailsService;
@Autowired
private JwtFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request
.requestMatchers("/api/v1/forum/threads/**").permitAll()
.anyRequest().permitAll()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
public DaoAuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception{
AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
auth.authenticationProvider(authenticationProvider());
return auth.build();
}
}
JWT filter:
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
JwtUtil jwtUtil;
@Autowired
UserDetailsService userDetailsService;
private static final List<String> PUBLIC_URLS = List.of(
"/api/v1/forum/categories/**",
"/api/v1/forum/threads/**"
);
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
String path = request.getRequestURI();
// Проверяем, является ли URL публичным
boolean isPublic = PUBLIC_URLS.stream()
.anyMatch(publicUrl -> pathMatcher.match(publicUrl, path));
if (isPublic) {
filterChain.doFilter(request, response);
return;
}
if (jwt != null) {
if (jwtUtil.validateToken(jwt)) {
System.out.println(1);
String username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
System.out.println(2);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}catch (MalformedJwtException ex) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token");
} catch (ExpiredJwtException ex) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Expired JWT token");
} catch (UnsupportedJwtException ex) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unsupported JWT token");
} catch (IllegalArgumentException ex) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT claims string is empty");
} catch (Exception ex) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication error");
}
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
I thought that problem might be in JWT filter, which is not well writen, but it doesn't seem to be a thing. I have similar Get mapping on /api/v1/forum/categories, which returns all the categories without 401. I tried modifying security config and thread controller. Here is my Categories Controller:
@RestController
@RequestMapping("/api/v1/forum/categories")
public class CategoryController {
@Autowired
private ThreadCategoryService threadCategoryService;
@GetMapping
public ResponseEntity<List<ThreadCategory>> getAllCategories(){
List<ThreadCategory> categories = threadCategoryService.findAll();
return ResponseEntity.ok(categories);
}
@GetMapping("/{id}")
public ResponseEntity<ThreadCategory> getCategoryById(@PathVariable long id) {
Optional<ThreadCategory> category = threadCategoryService.findById(id);
if(category.isPresent()){
return ResponseEntity.ok(category.get());
}
return null;
}
@GetMapping("/title/{title}")
public ResponseEntity<Optional<List<ThreadCategory>>> getCategoriesByTitle(@PathVariable String title) {
Optional<List<ThreadCategory>> categories = threadCategoryService.findByName(title);
if(categories.isPresent()){
return ResponseEntity.ok(categories);
}
return null;
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> createCategory(@RequestBody ThreadCategory category) {
ThreadCategory created = threadCategoryService.createCategory(category);
return ResponseEntity.ok(created);
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> updateCategory(@PathVariable long id, @RequestBody ThreadCategory category) {
return threadCategoryService.findById(id)
.map(categoryToUpdate -> {
categoryToUpdate.setName(category.getName());
categoryToUpdate.setDescription(category.getDescription());
threadCategoryService.createCategory(category);
return ResponseEntity.ok(category);
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteCategory(@PathVariable Long id) {
threadCategoryService.deleteCategoryById(id);
return ResponseEntity.ok("Категория удалена");
}
}
Upvotes: 0
Views: 51
Reputation: 1274
You should also add /api/v1/forum/threads
(without any slashes) to the security exceptions in the SecurityConfig
and the JwtFilter
, currently that configurations set as public only the paths that has a suffix.
Upvotes: 0