I’m working on Spring Boot project based on microservices architecture on backend and Vue.js on frontend. Structure of my project is next:
For avoiding CORS error usually I add @CrossOrigin
annotation on to class and it works.
It was all good and has been working well, until I added security part with ability to login users.
What did I did:
1. To API Gateway that built on spring-cloud-gateway
I’ve added AuthFilter
that uses as interceptor to create and check JWT:
api-gateway/src/main/java/.../AuthFilter.java
@Component public class AuthFilter extends AbstractGatewayFilterFactory<AuthFilter.Config> { private final WebClient.Builder webClientBuilder; @Autowired public AuthFilter(WebClient.Builder webClientBuilder) { super(Config.class); this.webClientBuilder = webClientBuilder; } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { if(!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { throw new RuntimeException("Missing auth information"); } String authHeader = exchange.getRequest().getHeaders().get(org.springframework.http.HttpHeaders.AUTHORIZATION).get(0); String[] parts = authHeader.split(" "); if(parts.length != 2 || !"Bearer".equals(parts[0])) { throw new RuntimeException("Incorrect auth structure"); } return webClientBuilder.build() .post() .uri("http://manager-service/api/v1/auth/validateToken?token=" + parts[1]) .retrieve() .bodyToMono(EmployeeDTO.class) //EmployeeDTO.class is custom DTO that represents User .map(user -> { exchange.getRequest() .mutate() .header("x-auth-user-id", user.getId()); return exchange; }).flatMap(chain::filter); }; } public static class Config { //live it empty because we dont need any particular configuration } }
2. I’ve added AuthFilter
as filter to each service in application.properties
:
api-gateway/src/resource/application.properties
##Workshop service routes spring.cloud.gateway.routes[0].id=workshop-service spring.cloud.gateway.routes[0].uri=lb://workshop-service spring.cloud.gateway.routes[0].predicates[0]=Path=/api/v1/workshop/** spring.cloud.gateway.routes[0].filters[0]=AuthFilter ##Manage service routes spring.cloud.gateway.routes[1].id=manager-service spring.cloud.gateway.routes[1].uri=lb://manager-service spring.cloud.gateway.routes[1].predicates[0]=Path=/api/v1/manage/** spring.cloud.gateway.routes[1].filters[0]=AuthFilter ##Manage service for singIn. Here we dont need to add AuthFilter, cause sign in page should be available for all spring.cloud.gateway.routes[2].id=manager-service-sign-in spring.cloud.gateway.routes[2].uri=lb://manager-service spring.cloud.gateway.routes[2].predicates[0]=Path=/api/v1/auth/signIn ...
3. Manager-service microservice used to control base entities for system, such as users, roles, organizations where users working are and so on, so here I added SecurityConfig
and WebConfig
, because this microservice will be responsible for JWT generating:
manager-service/src/main/java/.../SecurityConfig.java
@EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable() .authorizeRequests().anyRequest().permitAll(); return httpSecurity.build(); } }
manager-service/src/main/java/.../WebConfig.java
@EnableWebMvc public class WebConfig implements WebMvcConfigurer { private static final Long MAX_AGE=3600L; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedHeaders( HttpHeaders.AUTHORIZATION, HttpHeaders.CONTENT_TYPE, HttpHeaders.ACCEPT) .allowedMethods( HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.DELETE.name()) .maxAge(MAX_AGE) .allowedOrigins("http://localhost:8100") .allowCredentials(false); } }
4. In controller, that represents auth I also added @CrossOrigin
annotation to class:
manager-service/src/main/java/.../AuthController.java
@RestController @RequestMapping("api/v1/auth") @CrossOrigin(origins = "http://localhost:8100") @Slf4j public class AuthController { private final AuthService authService; @Autowired public AuthController(AuthService authService) { this.authService = authService; } @PostMapping("/signIn") public ResponseEntity<EmployeeDTO> signIn(@RequestBody CredentialsDTO credentialsDTO) { log.info("Trying to login {}", credentialsDTO.getLogin()); return ResponseEntity.ok(EmployeeMapper.convertToDTO(authService.signIn(credentialsDTO))); } @PostMapping("/validateToken") public ResponseEntity<EmployeeDTO> validateToken(@RequestParam String token) { log.info("Trying to validate token {}", token); Employee validatedTokenUser = authService.validateToken(token); return ResponseEntity.ok(EmployeeMapper.convertToDTO(validatedTokenUser)); } }
5. For frontend I use Vue.js. For requests I use axios
. Here are post
-request to login:
axios.post('http://localhost:8080/api/v1/auth/signIn', this.credentials).then(response => { console.log('response = ', response) console.log('token from response', response.data.token) this.$store.commit('saveToken', response.data.token) }).catch(error => { console.log('Error is below') console.log(error) })
All what I’m getting is an error: Access to XMLHttpRequest at 'http://localhost:8080/api/v1/auth/signIn' from origin 'http://localhost:8100' 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.
. Below you’ll see headers, that displays Chrome with request:
I’ve been trying to add another one corsConfiguration
, tried to mark with CrossOrigin
annotation only method, not class at all but it hadn’t take any effects. If I try to make such requests with postman it gives me expected response with generated token.
I’ll be grateful for any idea what could I do wrong.
Thanks!
UPDATE: As I understood well – all problems is in api-gateway
. If I make requests directly to service – I get right response, but if I make request through gateway – I’m facing an error, logs of api-gateway
below:
2022-07-05 00:34:18.128 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/api/v1/workshop/**]" does not match against value "/api/v1/auth/signIn" 2022-07-05 00:34:18.129 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/api/v1/manage/**]" does not match against value "/api/v1/auth/signIn" 2022-07-05 00:34:18.129 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "/api/v1/auth/signIn" matches against value "/api/v1/auth/signIn" 2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : Route matched: manager-service-sign-in 2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : Mapping [Exchange: OPTIONS http://localhost:8080/api/v1/auth/signIn] to Route{id='manager-service-sign-in', uri=lb://manager-service, order=0, predicate=Paths: [/api/v1/auth/signIn], match trailing slash: true, gatewayFilters=[], metadata={}} 2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : [e5b87280-8] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@78df1cfc
Advertisement
Answer
After research I’ve solved problem. It was all Gateway’s fault
As I mentioned before, direct request gives me right response, but only if I go through api-gateway
it gives me an errors.
So solution is to add CORS Configuration rules to gateway:
spring: cloud: gateway: default-filters: - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE globalcors: corsConfigurations: '[/**]': allowedOrigins: "http://localhost:8100" allowedHeaders: "*" allowedMethods: "*"
Please, note that if you don’t add section with gateway: default-filters
you will be facing similar error with header that contains multiple values.
Thanks to answer by Pablo Aragonés in another question and Spring Cloud Documentation