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.
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
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.
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