I have following api:
#ApiOperation(value = "Search product by text")
#PostMapping("/get/search")
public ResponseEntity<List<ShopProductDTO>> get(#RequestBody SearchProductRequestDTO search) {
//searching product here using search.getSearchText() value
}
Via postman I am sending:
{"searchText":"Утюг"}
But what I am receiving/seeing in logs:
SearchProductRequestDTO{searchText='РЈС‚СРі'}
After enabling DEBUG I see Http11InputBuffer logs where body:
{"searchText":"ГђВЈГ‘<U+0082>Г‘<U+008E>ГђВі"}
What I have done (none of them helped):
Added following properties in application.properties
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.messages.basename=messages
spring.messages.encoding=UTF-8
Exposed CharacterEncodingFilter
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public CharacterEncodingFilter charsetFilter() {
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceEncoding(true);
return filter;
}
Executed jar file with -Dfile.encoding=UTF-8 param
Included following headers in Postman
accept-charset:utf-8
content-type:application/json;charset=utf-8
What else I should do? Or am I missing something?
Try to change your code to this:
#ApiOperation(value = "Search product by text")
#PostMapping(value="/get/search", consumes="application/json;charset=UTF-8")
public ResponseEntity<List<ShopProductDTO>> get(#RequestBody SearchProductRequestDTO search) {
//searching product here using search.getSearchText() value
}
The change is in your line
#PostMapping(value="/get/search", consumes="application/json;charset=UTF-8")
If the issue is logging, change logger encoding to support UTF-8
#encoding- Over-ride the default character-encoding scheme.
logging.console.encoding=UTF-8
Related
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
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
add this to applications.yml in your gateway, solved issue for me:
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-
Allow-Credentials, RETAIN_UNIQUE
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "http://localhost:<your port>"
allowedHeaders: "*"
allowedMethods: "*"
I have faced similar issue & got resolved by defining following bean in my Configuration.
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://localhost:8100")); // Provide list of origins if you want multiple origins
config.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH"));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
You can comment out all other details e.g. #CrossOrigin(origins = "http://localhost:8100"), WebConfig.java, SecurityConfig.java as once we define above bean these things are not required.
Your code may not be running because you have defined bean , security config as well as webconfig which might be conflicting while processing your request.
I've created a Restful service with Spring MVC as shown below. I called it using Postman. I placed a breakpoint on 'return "hello World"'. There's no hit on the breakpoint with the error message "Required request part 'file' is not present".
However, if I comment out the '#RequestParam("file")' annotation, the breakpoint is hit with the parameter "file" being null.
What could have gone wrong? Very puzzled.
#RestController
#RequestMapping("/dp")
public class DpWebService implements IDpWebService {
#Override
#Bean
public MultipartConfigElement multipartConfigElement() {
return new MultipartConfigElement("");
}
#Override
#Bean
public MultipartResolver multipartResolver() {
org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new org.springframework.web.multipart.commons.CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(1000000);
return multipartResolver;
}
#Override
#RequestMapping(path = "/send", method = RequestMethod.POST, consumes = "multipart/form-data")
public String sendManifest(#RequestParam("file") MultipartFile file) {
return "Hello World";
}
}
Postman
Postman Header
Check your POSTMAN request Configuration. I think you have not changed the input type to File from Text. Uploading images, check the images. Hover the mouse over that area in Postman and select File from the drop-down menu.
Having Beans defined in your RestController is not a good design. Please separate out a Configuration class with #Configuration annotation and define your beans. The reasons being: Single Responsibility Principle - each class should only do about one thing.
https://java-design-patterns.com/principles/#single-responsibility-principle
#RequestParam might not be working for you because of the nature of the data that is contained in the file that you are sending through the request. RequestParam is likely to be used with name-value form fields. For complex data like json/xml it is advisable to use #RequestPart instead.
Instead of the #RequestParam annotation use the #RequestPart annotation.
Annotation that can be used to associate the part of a
"multipart/form-data" request with a method argument.
Try using it like :
#RestController
#RequestMapping("/dp")
public class DpWebService implements IDpWebService {
#Override
#Bean
public MultipartConfigElement multipartConfigElement() {
return new MultipartConfigElement("");
}
#Override
#Bean
public MultipartResolver multipartResolver() {
org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new org.springframework.web.multipart.commons.CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(1000000);
return multipartResolver;
}
#Override
#RequestMapping(path = "/send", method = RequestMethod.POST, consumes = "multipart/form-data")
public String sendManifest(#RequestPart("file") MultipartFile file) {
return "Hello World";
}
}
Also make sure that the request from the postman is getting triggered correctly :
Remove any un wanted request params from postman.
Make sure that under 'Body' tab the form-data is selected. Also make
sure that when selected the file in the key the name is provided as
'file' and type is also selected as file instead of text.
This is my working example.
#PostMapping("/uploadFile")
public UploadFileResponse uploadFile(#RequestParam("file") MultipartFile file) {
String fileName = fileStorageService.storeFile(file);
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath().path("/downloadFile/")
.path(fileName).toUriString();
return new UploadFileResponse(fileName, fileDownloadUri, file.getContentType(), file.getSize());
}
application.properties
## MULTIPART (MultipartProperties)
# Enable multipart uploads
spring.servlet.multipart.enabled=true
# Threshold after which files are written to disk.
spring.servlet.multipart.file-size-threshold=2KB
# Max file size.
spring.servlet.multipart.max-file-size=200MB
# Max Request Size
spring.servlet.multipart.max-request-size=215MB
## File Storage Properties
# Please change this to the path where you want the uploaded files to be stored.
file.upload-dir=C://Users//abc//Documents//
I have written a method with get request mapping it gives list of users. As jakson binding dependency is there is gives response in JSON. I've also dependency for XML which is Jackson Dataformat XML.So, if Accept is application/json it returns the response in JSON and if it is in application/xml it returns in XML.But by default it gives JSON response. SO, I wanted to add Accept header if not present and make it's default value as application/xml.
#GetMapping(path="/getAll")
public List<User> getUsers(#RequestHeader(value= "Accept" ,required=false, defaultValue="application/xml") String Accept)
{
return service.findAll();
}
But in above case, the header is not setting.
In order to do so, you need to modify your controller method to return ResponseEntity<List<User>> as following:
#GetMapping(path="/getAll")
public ResponseEntity<List<User>> getUsers(#RequestHeader(value= "Accept" ,required=false, defaultValue="application/xml") String Accept) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation(location);
responseHeaders.set("Accept", "Value");
return new ResponseEntity<List<User>>(service.findAll(), responseHeaders, HttpStatus.CREATED);
}
If you just want to respond XML from your spring boot application, use the following custom webMvcConfiguration. Setting a default Accept header just to respond XML does not seem like a good idea.
#Configuration
public class WebMvcConfiguration {
#Bean
public WebMvcConfigurer myWebMvcConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_XML);
}
};
}
}
if you are already using a custom WebMvcConfigurerAdapter, just override the configureContentNegotiation(...) method as above.
I tried to enable CORS filter on my java play app. I'm accessing localhost using angularjs to fetch some data. Here what I did in my play app side,
application.conf
play.http.filters = "com.de.filters.Filters"
play.filters.cors {
allowedOrigins = null
allowedHttpMethods = ["GET", "POST"]
allowedHttpHeaders = ["Accept"]
preflightMaxAge = 3 days
}
Filters.java
public class Filters implements HttpFilters {
#Inject
CORSFilter corsFilter;
public EssentialFilter[] filters() {
return new EssentialFilter[]{corsFilter};
}
}
When I'm trying to call my service, It's giving me an error,
XMLHttpRequest cannot load http://localhost:9000/app/search/0/10. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access. The response had HTTP status code 403.
How may I fix this?
From the Play 2.3 Documentation,
first add your filters to the libraryDependencies list in the build.sbt as stated below.
libraryDependencies += filters
Then, in the application.conf, don't set play.filters.cors.allowedOrigins as [null], it defaults to "all origins are allowed". Set a valid domain list or just ignore it. Don't forget to reference your filters:
play.http.filters = "filters.Filters"
At last, in your Filters.filter() method, write as below, specifying asJava() method, as stated in the docs.
public EssentialFilter[] filters() {
return new EssentialFilter[] {
corsFilter.asJava()
};
}
I finally got my CORSFilter to work. There is very little documentation on this and what is there doesn't work. Hope this helps someone out. (Play 2.5.4)
import com.google.inject.Inject;
import play.http.HttpFilters;
import play.mvc.EssentialAction;
import play.mvc.EssentialFilter;
import play.filters.cors.CORSFilter;
public class MyFilters extends EssentialFilter implements HttpFilters {
#Inject
private CORSFilter corsFilter;
#Override
public EssentialAction apply(EssentialAction next) {
return corsFilter.asJava().apply(next);
}
#Override
public EssentialFilter[] filters() {
EssentialFilter[] result = new EssentialFilter[1];
result[0] = this;
return result;
}
}
Also added this into the application.conf
play.filters.cors{
# allow all paths
pathPrefixes = ["/"]
# allow all origins
allowedOrigins = null
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE"]
# allow all headers
allowedHttpHeaders = null
}
Add these lines into application.conf instead.
play.filters.cors {
# allow all paths
pathPrefixes = ["/"]
# allow all origins
allowedOrigins = null
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE"]
# allow all headers
allowedHttpHeaders = null
}
You can refer this answer of me for more info.
I'm trying to accomplish a multipart file upload using feign, but I can't seem to find a good example of it anywhere. I essentially want the HTTP request to turn out similar to this:
...
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
Content-Disposition: form-data; name="name"
Larry
--AaB03x
Content-Disposition: form-data; name="file"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
Or even...
------fGsKo01aQ1qXn2C
Content-Disposition: form-data; name="file"; filename="file.doc"
Content-Type: application/octet-stream
... binary data ...
------fGsKo01aQ1qXn2C--
Do I need to manually build the request body, including generating the multipart boundaries? That seems a bit excessive considering everything else this client can do.
No, you don't. You just need to define a kind of proxy interface method, specify the content-type as: multipart/form-data and other info such as parameters required by the remote API. Here is an example:
public interface FileUploadResource {
#RequestLine("POST /upload")
#Headers("Content-Type: multipart/form-data")
Response uploadFile(#Param("name") String name, #Param("file") File file);
}
The completed example can be found here: File Uploading with Open Feign
For spring boot 2 and spring-cloud-starter-openfeign use this code:
#PostMapping(value="/upload", consumes = "multipart/form-data" )
QtiPackageBasicInfo upload(#RequestPart("package") MultipartFile package);
You need to change #RequestParam to #RequestPart in the feign client call to make it work, and also add consumes to the #PostMapping.
MBozic solution not full, you will also need to enable an Encoder for this:
public class FeignConfig {
#Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
#Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}
#FeignClient(name = "file", url = "http://localhost:8080", configuration = FeignConfig.class)
public interface UploadClient {
#PostMapping(value = "/upload-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String fileUpload(#RequestPart(value = "file") MultipartFile file);
}
If you are already using Spring Web, you can try my implementation of a Feign Encoder that is able to create Multipart requests. It can send a single file, an array of files alongwith one or more additional JSON payloads.
Here is my test project. If you don't use Spring, you can refactor the code by changing the encodeRequest method in FeignSpringFormEncoder.
Let me add Answer for latest OpenFeign :
Add dependency for Feign-Form:
io.github.openfeign.form
feign-form
3.8.0
Add FormEncoder to your Feign.Builder like so:
SomeApi github = Feign.builder()
.encoder(new FormEncoder())
.target(SomeApi.class, "http://api.some.org");
API endpoint
#RequestLine("POST /send_photo")
#Headers("Content-Type: multipart/form-data")
void sendPhoto (#Param("is_public") Boolean isPublic, #Param("photo") FormData photo);
Refer : https://github.com/OpenFeign/feign-form
Call from one service to another service for file transfer/upload/send using feign client interface:
#FeignClient(name = "service-name", url = "${service.url}", configuration = FeignTokenForwarderConfiguration.class)
public interface UploadFeignClient {
#PostMapping(value = "upload", headers = "Content-Type= multipart/form-data", consumes = "multipart/form-data")
public void upload(#RequestPart MultipartFile file) throws IOException;
}
**Actual API:**
#RestController
#RequestMapping("upload")
public class UploadController {
#PostMapping(value = "/upload", consumes = { "multipart/form-data" })
public void upload(#RequestParam MultipartFile file) throws IOException {
//implementation
}
}