Мне тяжело с Spring Security.В основном у меня есть /auth/signup
и /auth/login
для создания учетной записи и получения токена jwt соответственно.
Оба прекрасно работают локально через Insomnia / POSTman , но при отправке запроса на /auth/login
изинтерфейс React.js с fetch()
либо возвращает ошибку запроса предпечатной проверки CORS, либо дает ответ 200 «непрозрачный» или «cors» OK с без тела .Последний бесполезен, так как мне нужен токен.Что раздражает, так это то, что он работает на бессоннице.
Вот как выглядит ответ, когда он запускается через fetch()
из javascript локально.Там нет тела и нет заголовков.Где, как и в случае с бессонницей или POSTman, есть заголовки и тело.

За последние несколько недель я пробовал несколько разных вещей, включая:
- Добавление пользовательского
addCorsMappings()
вызова в значении WebMvcConfigurer
- CORS фильтр
@Component
, который переопределяет doFiler()
и имеет набор setHeader()
вызовов. - Класс конфигурации CORS с
corsConfigurer()
как @Bean
- Другой
Bean
внутри пользовательского WebSecurityConfigurerAdapter
, возвращающий CorsConfigurationSource
ПравдаЯ не знаю, какой из них самый современный, правильный для использования.Я прочитал так много вещей (в основном статьи и руководства по переполнению стека), которые не помогли.Это так неправильно, просто вставлять фрагменты без полного понимания того, сработает ли это или как.Если кто-нибудь может описать правильный способ настройки CORS для работы с fetch()
вызовом из React, а не только с POSTman / Insomia - я был бы очень признателен.
Я включу столько источников, сколько смогу, но исключит такие вещи, как пользовательские классы здесь.Насколько я понимаю, они довольно стандартны, по большей части.
WebMvcConfig.java
:
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 360;
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("Content-Type", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Authorization", "X-Requested-With", "requestId", "Correlation-Id")
.exposedHeaders("Content-Type", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Authorization", "X-Requested-With", "requestId", "Correlation-Id")
.allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE")
.maxAge(MAX_AGE_SECS);
}
}
SimpleCORSFilter.java
:
@Component
public class SimpleCORSFilter implements Filter {
public SimpleCORSFilter() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "origin, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Authorization, requestId, Correlation-Id, Content-Type, Accept, X-Requested-With, remember-me");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}
CorsConfig.java
:
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("OPTIONS", "GET", "POST", "PUT", "DELETE").allowedOrigins("*")
.allowedHeaders("*");
}
};
}
}
Теперь вот класс SecurityConfig.java
, использующий WebSecurityConfigurerAdapter
.Обратите внимание на методы configure(HttpSecurity http)
и CorsConfigurationSource corsConfigurationSource()
.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.
userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/auth/**")
.permitAll()
.antMatchers("/user/checkUsernameAvailability", "/user/checkEmailAvailability")
.permitAll()
.antMatchers(HttpMethod.GET, "/polls/**", "/users/**")
.permitAll()
.anyRequest()
.authenticated();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token", "Content-Type", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Authorization", "X-Requested-With", "requestId", "Correlation-Id"));
configuration.setExposedHeaders(Arrays.asList("x-auth-token"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
У меня есть эти два маршрута в контроллере /auth
:
@CrossOrigin
@PostMapping(path = "/login", produces = { "application/json" }, consumes = { "application/json" })
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest){
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsernameOrEmail(),
loginRequest.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok().body(new JwtAuthenticationResponse(jwt));
}
@CrossOrigin
@PostMapping(path = "/signup", produces = { "application/json" }, consumes = { "application/json" })
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest){
if (userClient.existsByUsername(signUpRequest.getUsername())) {
return new ResponseEntity(new ApiResponse(false, "Username already being used!"),HttpStatus.BAD_REQUEST);
}
AppUser user = new AppUser(signUpRequest.getUsername(), signUpRequest.getPassword(), "ADMIN");
user.setPassword(passwordEncoder.encode(user.getPassword()));
AppUser result = userClient.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentContextPath().path("/users/{useranme}").buildAndExpand(result.getUsername()).toUri();
return ResponseEntity.created(location).body(new ApiResponse(true, "Success: User registered"));
}
Наконец вызов вызова javascript:
fetch(`${config.apiUrl}/auth/login`, {
method: 'POST',
mode: 'cors',
headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'accept':'*/*' },
body: JSON.stringify({ "usernameOrEmail": username, "password": password })
})