Search code examples
reactjsspring-bootaxioscors

How can I resolve CORS error in Spring Boot for specific controllers?


Hello Stack Overflow community,

I am encountering a CORS (Cross-Origin Resource Sharing) error in my Spring Boot application, and I need some guidance on resolving it. The issue is that the error only occurs for specific controllers within my application. Here's the scenario:

I have a Spring Boot application with multiple controllers, and I have configured CORS globally in my application using @CrossOrigin annotations in my controllers. However, one of my controllers is still generating CORS errors, while the other work fine.

this is the controller not working:

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.CrossOrigin;


import com.sinandemir.todoapp.dto.requests.TodoRequest;
import com.sinandemir.todoapp.dto.responses.TodoResponse;
import com.sinandemir.todoapp.services.TodoService;

@RestController
@RequestMapping("api/v1/todos")
@CrossOrigin("*")
public class TodoController {

    private TodoService todoService;

    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping
    public ResponseEntity<TodoResponse> addTodo(@RequestBody TodoRequest todoRequest) {
        TodoResponse savedTodo = todoService.addTodo(todoRequest);
        return new ResponseEntity<TodoResponse>(savedTodo, HttpStatus.CREATED);
    }

    @PreAuthorize("hasAnyRole('ADMIN','USER')")
    @GetMapping("{todoId}")
    public ResponseEntity<TodoResponse> getTodoById(@PathVariable Long todoId) {
        TodoResponse todo = todoService.getTodo(todoId);
        return new ResponseEntity<TodoResponse>(todo, HttpStatus.OK);
    }

    @PreAuthorize("hasAnyRole('ADMIN','USER')")
    @GetMapping
    public ResponseEntity<Page<TodoResponse>> getAllTodosWithPagination(Pageable pageable) {
        Page<TodoResponse> todos = todoService.getAllTodosWithPagination(pageable);
        return new ResponseEntity<Page<TodoResponse>>(todos, HttpStatus.OK);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PutMapping("{todoId}")
    public ResponseEntity<TodoResponse> updateTodo(@RequestBody TodoRequest todoRequest, @PathVariable Long todoId) {
        TodoResponse todo = todoService.updateTodo(todoRequest, todoId);
        return new ResponseEntity<TodoResponse>(todo, HttpStatus.OK);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("{todoId}")
    public ResponseEntity<String> deleteTodo(@PathVariable Long todoId) {
        todoService.deleteTodo(todoId);
        return new ResponseEntity<String>("todo deleted successfully.", HttpStatus.OK);
    }

    @PreAuthorize("hasAnyRole('ADMIN','USER')")
    @PatchMapping("{todoId}")
    public ResponseEntity<TodoResponse> changeCompletedStatus(@PathVariable Long todoId) {
        TodoResponse todo = todoService.changeCompletedStatus(todoId);
        return new ResponseEntity<TodoResponse>(todo, HttpStatus.OK);
    }
}

this is the controller working:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sinandemir.todoapp.dto.requests.RefreshTokenRequest;
import com.sinandemir.todoapp.dto.requests.UserLoginRequest;
import com.sinandemir.todoapp.dto.requests.UserRegisterRequest;
import com.sinandemir.todoapp.dto.responses.UserLoginResponse;
import com.sinandemir.todoapp.dto.responses.UserRegisterResponse;
import com.sinandemir.todoapp.services.AuthService;
import com.sinandemir.todoapp.services.RefreshTokenService;

import io.jsonwebtoken.ExpiredJwtException;

@RestController
@RequestMapping("api/v1/auth")
@CrossOrigin("*")
public class AuthController {

    private AuthService authService;
    private RefreshTokenService refreshTokenService;

    public AuthController(AuthService authService, RefreshTokenService refreshTokenService) {
        this.authService = authService;
        this.refreshTokenService = refreshTokenService;
    }

    @PostMapping("register")
    public ResponseEntity<UserRegisterResponse> register(@RequestBody UserRegisterRequest registerRequest) {
        UserRegisterResponse user = authService.register(registerRequest);
        return new ResponseEntity<UserRegisterResponse>(user, HttpStatus.CREATED);
    }

    @PostMapping("login")
    public ResponseEntity<UserLoginResponse> login(@RequestBody UserLoginRequest loginRequest) {
        UserLoginResponse user = authService.login(loginRequest);
        return new ResponseEntity<UserLoginResponse>(user, HttpStatus.OK);
    }

    @PostMapping("refresh-token")
    public ResponseEntity<String> refreshToken(@RequestBody RefreshTokenRequest refreshTokenRequest) {
        try {
            String newJwtToken = authService.refreshAccessToken(refreshTokenRequest.getRefreshToken());
            if (newJwtToken != null) {
                return new ResponseEntity<String>(newJwtToken, HttpStatus.CREATED);
            } else {
                return new ResponseEntity<String>("Token refresh failed.", HttpStatus.BAD_REQUEST);
            }
        } catch (ExpiredJwtException e) {
            refreshTokenService.deleteRefreshTokenByRefreshToken(refreshTokenRequest.getRefreshToken());
            return new ResponseEntity<String>("Session has expired.", HttpStatus.UNAUTHORIZED);
        }
    }
}

this is my Spring security config class:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.sinandemir.todoapp.security.JwtAuthenticationEntryPoint;
import com.sinandemir.todoapp.security.JwtAuthenticationFilter;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    
    private UserDetailsService userDetailsService;
    private JwtAuthenticationEntryPoint authenticationEntryPoint;
    private JwtAuthenticationFilter authenticationFilter;

    public SecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationEntryPoint authenticationEntryPoint, JwtAuthenticationFilter authenticationFilter){
        this.userDetailsService = userDetailsService;
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.authenticationFilter = authenticationFilter;
    }

    @Bean
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf((csrf) -> csrf.disable())
                .authorizeHttpRequests((authorize) -> {
                    authorize.requestMatchers("api/v1/auth/**").permitAll();
                    authorize.anyRequest().authenticated();
                }).httpBasic(Customizer.withDefaults());

                http.exceptionHandling(exception -> exception.authenticationEntryPoint(authenticationEntryPoint));

                http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
                
        return http.build();
    }

    @Bean
    public AuthenticationManager manager(AuthenticationConfiguration configuration) throws Exception{
        return configuration.getAuthenticationManager();
    }
}

another way i tried for cors error:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:5173")
                .allowedMethods("*")
                .allowedHeaders("*");
    }

}

this is request from my frontend i got cors error:

export const getTodosWithPagination = async (
  token: string,
  page: number,
  size: number
) => {
  try {
    const data = await axios.get(BASE_URL + `todos?page=${page}&size=${size}`, {
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token}`,
      },
    });

    const response = data.data;
    return response;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      if (error.response?.status === 401) {
        throw new Error("Geçersiz token.");
      }
      if (error.response?.status !== 201) {
        throw new Error("Bilinmeyen bir hata!");
      }
    } else return error;
  }
};

my useEffect in ReactJs:

   useEffect(() => {
        const token = cookies.auth?.accessToken
        const getTodos = async () => {
            if (token) {
                const response = await getTodosWithPagination(token, 0, 10)
                console.log(response);
            }
            return null
        }

        getTodos()
    }, [])

screenshot of the error I get

thank you all!!!!!!!

SOLVED! you can solve as in comment or you can edit securityFilterChaine like this:


     @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf((csrf) -> csrf.disable())
                .authorizeHttpRequests((authorize) -> {
                    authorize.requestMatchers("api/v1/auth/**").permitAll();
                    authorize.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll(); // added new line
                    authorize.anyRequest().authenticated();
                }).httpBasic(Customizer.withDefaults());

        http.exceptionHandling(exception -> exception.authenticationEntryPoint(authenticationEntryPoint));

        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }


Solution

  • With Spring security, CORS makes a preflight request that does not contain any cookies. You need to enable CORS support through Spring security.

    @EnableWebSecurity
    public class WebSecurityConfig {
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.cors(cors -> cors.configurationSource(corsConfigurationSource()))...
        }
    }
    

    And configure CorsConfigurationSource bean like:

        @Bean
        CorsConfigurationSource corsConfigurationSource()
        {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
            configuration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "OPTIONS"));
    configuration.setAllowedHeaders(List.of("*"));
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }