Как правильно настроить CORS в spring-boot приложении с JWT авторизацией
Кто хорошо разбирается в spring-security и CORS помогите пожалуйста. К сожалению я не знаю как проверить работоспособность CORS. Надо чтобы все методы и все Header'ы были разрешены. Backend который предоставляет api я запустил на Dockere на Windows. Проблема возникает стороне фронтенд разработчика (vue.js) на эндпоинте "api/login" (POST метод, который реализуется самим spring-security насколько я понимаю. Сам я в контроллере данный эндопинт не создавал). Фронтэндщик создал вьюшку для ввода username и password, и когда нажимает кнопку "войти", то сперва до настройки CORS возникала ошибка:
Access to fetch at 'http://10.50.50.99:8080/api/login/' from origin 'http://127.0.0.1:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Потом в процессе переборки всевозможных вариантов настройки, то возвращал 401, то 404. И по-моему, с данными настройками стал возвращать 404. В конце концов я уж совсем запутался. Как видно из класса WebSecurityConfigurerImpl, есть два кастомных jwt-фильтра:
CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilterCustomAuthorizationFilter extends OncePerRequestFilter
Правильно ли я настроил CORS? Или как правильно настроить CORS?
Вот конфиг
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import ru.rcitsakha.motherforum.filter.CustomAuthenticationFilter;
import ru.rcitsakha.motherforum.filter.CustomAuthorizationFilter;
import java.util.Arrays;
import java.util.List;
import static org.springframework.http.HttpMethod.*;
@Configuration
@EnableWebSecurity
@PropertySource("classpath:application.properties")
@RequiredArgsConstructor
public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Value("${ru.rcitsakha.motherforum.jwtSecret}")
private String jwtSecret;
@Value("${rcit.sakha.motherforum.access_jwtExpirationMs}")
private long accessTokenTime;
@Value("${rcit.sakha.motherforum.refresh_jwtExpirationMs}")
private long refreshTokenTime;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// передается authenticationManagerBean()!
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean());
customAuthenticationFilter.setJwtSecret(jwtSecret);
customAuthenticationFilter.setAccessTokenTime(accessTokenTime);
customAuthenticationFilter.setRefreshTokenTime(refreshTokenTime);
customAuthenticationFilter.setFilterProcessesUrl("/api/login");
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeHttpRequests().antMatchers("/api/login/**", "/token/refresh", "api/motherforum/**").permitAll();
// User managing
http.authorizeHttpRequests().antMatchers(POST, "/api/users/**", "/api/user/save/**", "/api/role/**").hasAuthority("ROLE_SUPER_ADMIN");
// Only admin can access to Mother's data
http.authorizeHttpRequests().antMatchers(GET, "/api/mothers/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeHttpRequests().antMatchers(POST, "/api/mothers/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeHttpRequests().antMatchers(PUT, "/api/mothers/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeHttpRequests().antMatchers(DELETE, "/api/mothers/**").hasAnyAuthority("ROLE_ADMIN");
// Only admin can access to Masterclasses data with information about Mothers
http.authorizeHttpRequests().antMatchers(GET, "/api/masterclasses/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeHttpRequests().antMatchers(POST, "/api/masterclasses/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeHttpRequests().antMatchers(PUT, "/api/masterclasses/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeHttpRequests().antMatchers(DELETE, "/api/masterclasses/**").hasAnyAuthority("ROLE_ADMIN");
http.addFilter(customAuthenticationFilter);
CustomAuthorizationFilter customAuthorizationFilter = new CustomAuthorizationFilter();
customAuthorizationFilter.setJwtSecret(jwtSecret);
http.addFilterBefore(customAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
А это Main.class
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
// In Main.class to avoid cycle dependency
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
CustomAuthenticationFilter
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private String jwtSecret;
private long accessTokenTime;
private long refreshTokenTime;
public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setJwtSecret(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
public void setAccessTokenTime(long accessTokenTime) {
this.accessTokenTime = accessTokenTime;
}
public void setRefreshTokenTime(long refreshTokenTime) {
this.refreshTokenTime = refreshTokenTime;
}
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
log.info("username is: {}", username);
log.info("Password is: {}", password);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authenticationToken);
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication) throws IOException {
// User from SpringSecurity class!
User user = (User) authentication.getPrincipal();
// instead secret use more complex example
Algorithm algorithm = Algorithm.HMAC256(jwtSecret.getBytes());
String access_token = JWT.create()
.withSubject(user.getUsername()) // username should be Unique!
.withExpiresAt(new Date(System.currentTimeMillis() + accessTokenTime))
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList())
)
.sign(algorithm);
String refresh_token = JWT.create()
.withSubject(user.getUsername()) // username should be Unique!
.withExpiresAt(new Date(System.currentTimeMillis() + refreshTokenTime)) // refresh token living 30 minutes
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList())
)
.sign(algorithm);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", access_token);
tokens.put("refresh_token", refresh_token);
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}
}
CustomAuthorizationFilter
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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 java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static java.util.Arrays.stream;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j
public class CustomAuthorizationFilter extends OncePerRequestFilter {
private String jwtSecret;
public void setJwtSecret(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/token/refresh/**")) {
filterChain.doFilter(request, response);
} else {
String authorizationHeader = request.getHeader(AUTHORIZATION);
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
try {
String token = authorizationHeader.substring("Bearer ".length());
Algorithm algorithm = Algorithm.HMAC256(jwtSecret.getBytes()); // should be same with authenticationFilter
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String username = decodedJWT.getSubject();
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
stream(roles).forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role));
});
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (Exception exception) {
log.error("Error logging in: {}", exception.getMessage());
response.setHeader("error", exception.getMessage());
response.setStatus(FORBIDDEN.value());
Map<String, String> error = new HashMap<>();
error.put("error_message", exception.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
} else {
filterChain.doFilter(request, response);
}
}
}
}