So we're trying to use the OpenAPI generator and so far we've had mixed results.
Steps to reproduce:
Download openapi generator jar: wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator/4.0.3/openapi-generator-4.0.3.jar
Generate springboot server for the petstore example: java -jar openapi-generator-cli-4.0.3.jar generate -g spring -i https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml
You'll end up with controller classes that look like this:
package org.openapitools.api;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.NativeWebRequest;
import java.util.Optional;
#javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2019-08-06T15:08:49.070+01:00[Europe/London]")
#Controller
#RequestMapping("${openapi.swaggerPetstore.base-path:/v1}")
public class PetsApiController implements PetsApi {
private final NativeWebRequest request;
#org.springframework.beans.factory.annotation.Autowired
public PetsApiController(NativeWebRequest request) {
this.request = request;
}
#Override
public Optional<NativeWebRequest> getRequest() {
return Optional.ofNullable(request);
}
}
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (4.0.3).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package org.openapitools.api;
import org.openapitools.model.Error;
import org.openapitools.model.Pet;
import io.swagger.annotations.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
#javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2019-08-06T15:08:49.070+01:00[Europe/London]")
#Validated
#Api(value = "pets", description = "the pets API")
public interface PetsApi {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
#ApiOperation(value = "Create a pet", nickname = "createPets", notes = "", tags={ "pets", })
#ApiResponses(value = {
#ApiResponse(code = 201, message = "Null response"),
#ApiResponse(code = 200, message = "unexpected error", response = Error.class) })
#RequestMapping(value = "/pets",
produces = { "application/json" },
method = RequestMethod.POST)
default ResponseEntity<Void> createPets() {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
#ApiOperation(value = "List all pets", nickname = "listPets", notes = "", response = Pet.class, responseContainer = "List", tags={ "pets", })
#ApiResponses(value = {
#ApiResponse(code = 200, message = "A paged array of pets", response = Pet.class, responseContainer = "List"),
#ApiResponse(code = 200, message = "unexpected error", response = Error.class) })
#RequestMapping(value = "/pets",
produces = { "application/json" },
method = RequestMethod.GET)
default ResponseEntity<List<Pet>> listPets(#ApiParam(value = "How many items to return at one time (max 100)") #Valid #RequestParam(value = "limit", required = false) Integer limit) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
ApiUtil.setExampleResponse(request, "application/json", "null");
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
#ApiOperation(value = "Info for a specific pet", nickname = "showPetById", notes = "", response = Pet.class, tags={ "pets", })
#ApiResponses(value = {
#ApiResponse(code = 200, message = "Expected response to a valid request", response = Pet.class),
#ApiResponse(code = 200, message = "unexpected error", response = Error.class) })
#RequestMapping(value = "/pets/{petId}",
produces = { "application/json" },
method = RequestMethod.GET)
default ResponseEntity<Pet> showPetById(#ApiParam(value = "The id of the pet to retrieve",required=true) #PathVariable("petId") String petId) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
ApiUtil.setExampleResponse(request, "application/json", "null");
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
So my question is this: everything I can find implies that Spring Controllers are multi-threaded and may handle multiple requests at once. Is the code generator broken? Am I interpreting this completely wrong?
The constructor for PetsApiController gives me pause. If it's being autowired once per request then that implies that there's only one per request?
#org.springframework.beans.factory.annotation.Autowired
public PetsApiController(NativeWebRequest request) {
this.request = request;
}
The swagger code generators are notoriously bad, friend of mine said that the generators have the breadth but not the depth. You can generate skeletons for all sorts of languages and frameworks, but they have severe limitations. For example, try generating a good skeleton from a SwaggerDoc with Page<Something> or other Generics. I would very sadly say that they have almost no utility, and the tools tend to only work reliably the other way around, that is coding first and then generating the SwaggerDoc.
A place I worked at had a great concept I really liked whereby you would design your API first before implementing it, which sounds like you are trying to do. Some IDEs even support generated code, and there are plugins for build tools such as maven gradle etc to generate the code from your yaml.
But in practice I spent days trying to get desirable results from these tools and gave up. I think the real problem is Swagger/OpenAPI is still heavily viewed as documentation tool, not a design tool. I also think that trying to create an all encompassing project generator was setup to fail from the get-go.
I myself tried to customize the moustache templates which the generator used, but generics in Java were a nightmare, and you couldn't get the proper workflow working whereby I would change the SwaggerDoc and then update my code, as my approach was to generate an interface, and then implement that interface, but Annotations weren't inherited so I had to duplicate all the code anway meaning there was no benefit.
Related
I have been developing new APIs in Springboot 3 and it has been more a headache than something good, but finally I'm able to do something. Issue is that I was able to add Swagger to it, with OpenAPI from Spring-doc release 2. but the configuration file is not reading my properties. Also I have troubles trying to set up my bearer authentication....
This is my actual swagger: Swagger + spring-doc
And third issue related to this is... I keep can't make the swagger to read the default responses... even I configured like in the old versions, but I couldn't make it work...
For properties, I have tried to add them before the config class, and ad a Bean in methods.
For bearer, I was following Baeldung JWT Swagger guide, but It confuses me a little, tried to run but didnt work.
This is my OpenApiConfig class (commented lines are because they are not compatible with tag declaration):
package info.peluka.csaread.config;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
#Configuration
#OpenAPIDefinition(
info =#Info(
title = "${module-name}",
version = "${api-version}",
contact = #Contact(
name = "Joseph", email = "CSA_Read_API#peluka.info", url = "https://www.peluka.info"
),
license = #License(
name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0"
),
description = "${module-description}"
),
servers = #Server(
url = "${api.server.url}",
description = "Production"
)
)
public class OpenApiConfig {
private final String moduleName;
private final String apiVersion;
private final String moduleDescription;
public OpenApiConfig(
#Value("${module-name}") String moduleName,
#Value("${api-version}") String apiVersion,
#Value("${module-description}") String moduleDescription) {
this.moduleName = moduleName;
this.apiVersion = apiVersion;
this.moduleDescription = moduleDescription;
}
/**
* Configure the OpenAPI components.
*
* #return Returns fully configure OpenAPI object
* #see OpenAPI
*/
#Bean
public OpenAPI customizeOpenAPI() {
//#formatter:off
final String securitySchemeName = "bearerAuth";
return new OpenAPI()
.addSecurityItem(new SecurityRequirement()
.addList(securitySchemeName))
.components(new Components()
.addSecuritySchemes(securitySchemeName, new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.description(
"Provide the JWT token. JWT token can be obtained from the /token endpoint. If need to create an user, contact Griffith.")
.bearerFormat("JWT")));
//#formatter:on
}
// #Bean
// public OpenAPI customOpenAPI(#Value("${application-description}")
// String appDesciption,
// #Value("${application-version}")
// String appVersion) {
// return new OpenAPI()
// .info(new Info()
// .title("CSA Read API - Swagger")
// .version(appVersion)
// .description(appDesciption)
// .termsOfService("http://swagger.io/terms/")
// .license(new License().
// name("Apache 2.0").
// url("http://springdoc.org")));
// }
// #Bean
// public OpenAPI customOpenAPI() {
// final String securitySchemeName = "bearerAuth";
// return new OpenAPI()
// .addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
// .components(
// new Components()
// .addSecuritySchemes(securitySchemeName,
// new SecurityScheme()
// .name(securitySchemeName)
// .type(SecurityScheme.Type.HTTP)
// .scheme("bearer")
// .bearerFormat("JWT")
// )
// )
// .info(new Info().title(moduleName).version(apiVersion).description(moduleDescription));
// }
}
Inside my controller, I have this (It's just a code block of two endpoints) :
(...)
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
(...)
#RestController
#RequestMapping("/csa/api")
#Tag(name = "Users & Clan Controller", description = "This Endpoint manages Users and CSA Members")
public class ClanController extends Helper {
(...)
#PostMapping("/token")
#Operation(summary = "Request a token", description = "Return a new token" )
#ApiResponses(value = {
#ApiResponse(responseCode = "200", description = TOKEN_GENERATED_SUCCESSFULLY, content = #Content),
#ApiResponse(responseCode = "400", description = EMAIL_OR_PASSWORD_WRONG, content = #Content),
#ApiResponse(responseCode = "500", description = INTERNAL_SERVER_ERROR, content = #Content) })
public ResponseEntity<Object> token(#RequestParam("email") String email, #RequestParam("password") String password) {
try {
if(!isValidEmail(email))
return ResponseHandler.generateResponse(EMAIL_OR_PASSWORD_WRONG, HttpStatus.BAD_REQUEST, EMPTY);
var optionalUsers = usersRepository.findByEmailAndPassword(email, password);
if (!optionalUsers.isPresent())
return ResponseHandler.generateResponse(EMAIL_OR_PASSWORD_WRONG, HttpStatus.BAD_REQUEST, EMPTY);
var token = getJWTToken(email);
optionalUsers.get().setToken(token);
optionalUsers.get().setLastLogin(LocalDate.now());
usersRepository.save(optionalUsers.get());
return ResponseHandler.generateResponse(TOKEN_GENERATED_SUCCESSFULLY, HttpStatus.OK, new Token(token));
} catch (Exception e){
return ResponseHandler.generateResponse(INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
#PostMapping("/updatePW")
#Operation(summary = "Update user password", description = "Return successful if all validations were OK." )
#ApiResponses(value = {
#ApiResponse(responseCode = "201", description = PASSWORD_CHANGED_SUCCESSFULLY, content = #Content),
#ApiResponse(responseCode = "400", description = EMAIL_OR_PASSWORD_WRONG, content = #Content),
#ApiResponse(responseCode = "406", description = NEW_PASSWORD_ERROR, content = #Content),
#ApiResponse(responseCode = "500", description = INTERNAL_SERVER_ERROR, content = #Content) })
#SecurityRequirement(name = "Bearer Authentication")
public ResponseEntity<Object> updatePassword(#RequestBody OldUser oldUser){
Users userSaved;
try {
if(!isValidEmail(oldUser.getEmail()))
return ResponseHandler.generateResponse(EMAIL_OR_PASSWORD_WRONG, HttpStatus.BAD_REQUEST, oldUser);
if(!oldUser.getNewPassword().isEmpty() && !isValidPassword(oldUser))
return ResponseHandler.generateResponse(NEW_PASSWORD_ERROR, HttpStatus.NOT_ACCEPTABLE, oldUser);
var init = usersRepository.findAll();
var user = usersRepository.findByEmailAndPassword(oldUser.getEmail(), oldUser.getOldPassword());
if(!user.isPresent())
return ResponseHandler.generateResponse(EMAIL_OR_PASSWORD_WRONG, HttpStatus.BAD_REQUEST, oldUser);
user.get().setPassword(oldUser.getNewPassword());
if(!oldUser.getNewPassword().isEmpty()){
userSaved = usersRepository.save(user.get());
} else {
userSaved = usersRepository.save(new Users(user.get()));
}
emailService.sendMail(userSaved, EMAIL_CHANGE_PASSWORD);
return ResponseHandler.generateResponse(PASSWORD_CHANGED_SUCCESSFULLY, HttpStatus.CREATED, userSaved);
} catch (Exception exception) {
return ResponseHandler.generateResponse(INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
}
}
As you can see in the first image.... For some reasons my "tags" of spring-doc are not working. I have no descriptions, error responses, any definition at all.
I'm working with SpringBoot 3.0.0 and spring-doc version 2.0.0, I have in my pom the following related imported artifacts to spring-doc: springdoc-openapi-starter-webmvc-ui, springdoc-openapi-starter-common, springdoc-openapi-starter-webflux-ui
I'm using also Java 17, and recently I started to use Dockerfile (This is the only way I have to make Swagger works without asking me credentials)
Please, any help with this will be very useful. I have been trying to figure out what to do for several weeks now, and the final users need Swagger implemented for easier access....
PS1: The response of api has this format:
{
"data": {
"name": "TEST NAME",
"email": "TEST.EMAIL#EMAIL.io",
"password": "TEST_PASSWORD",
"dateCreated": "2022-12-13",
"dateModified": "2022-12-13",
"lastLogin": "2022-12-13",
"token": "Bearer TOKEN",
"active": true
},
"message": "User Created Successfully",
"status": 201
}
Basically is:
{
"data" : Object
"message" : String
"status" : Int
}
Where data is the object created in most of cases. Message, just a typo message. status, HTTP Code with the status of operation...
I am using the openapi-generator to create a multipart/form-data. In an ideal situation I should be able to upload a file, and specify in the options what should happen with the file.
I would like the options to be an object. For one or another reason, this does not seem to work. The openapi-generator generates the API interface, etc, but it does not generate the model for the options object.
I can specify the options individually, but I prefer the options to be an object, with the necessary model to it. I believe this provides a more structured way to deal with the options.
My yaml file looks like this (I specified what works and what doesn't work):
/fileuploadwithoptions:
post:
summary: Upload a file and processes it according to the options specified.
requestBody:
content:
multipart/form-data:
schema:
required:
- file
type: object
properties:
file:
type: string
format: binary
option1: <-- this works
type: string
description: A descriptions for option 1.
options: <-- this does not work
#type: application/json
type: object
description: The options.
properties:
option1:
type: string
description: A descriptions for option 1.
option2:
type: string
description: A descriptions for option 2.
encoding:
file:
contentType: application/octet-stream
This generates the following API:
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.0.1).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package com.teradact.tokenizerplusserver.api;
import com.teradact.tokenizerplusserver.model.FileuploadwithoptionsPostRequestOptions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Generated;
#Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-10-11T08:11:04.909499+02:00[Europe/Brussels]")
#Validated
#Tag(name = "fileuploadwithoptions", description = "the fileuploadwithoptions API")
public interface FileuploadwithoptionsApi {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
/**
* POST /fileuploadwithoptions : Upload a file and processes it according to the options specified.
*
* #param file (required)
* #param option1 A descriptions for option 1. (optional)
* #param options (optional)
* #return The file. (status code 200)
* or bad input parameter (status code 400)
*/
#Operation(
operationId = "fileuploadwithoptionsPost",
summary = "Upload a file and processes it according to the options specified.",
responses = {
#ApiResponse(responseCode = "200", description = "The processed file.", content = {
#Content(mediaType = "application/octet-stream", schema = #Schema(implementation = org.springframework.core.io.Resource.class))
}),
#ApiResponse(responseCode = "400", description = "bad input parameter")
}
)
#RequestMapping(
method = RequestMethod.POST,
value = "/fileuploadwithoptions",
produces = { "application/octet-stream" },
consumes = { "multipart/form-data" }
)
default ResponseEntity<org.springframework.core.io.Resource> fileuploadwithoptionsPost(
#Parameter(name = "file", description = "", required = true) #RequestPart(value = "file", required = true) MultipartFile file,
#Parameter(name = "option1", description = "A descriptions for option 1.") #Valid #RequestParam(value = "option1", required = false) String option1,
#Parameter(name = "options", description = "") #Valid #RequestParam(value = "options", required = false) FileuploadwithoptionsPostRequestOptions options
) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
This gives however the following error:
"Cannot resolve symbol 'FileuploadwithoptionsPostRequestOptions", since the model for the object is simply not created.
Thanks in advance for pointing out where I am wrong!
The interface has the name 'FileuploadwithoptionsApi', YAML file has /fileuploadwithoptions and the response entity has name fileuploadwithoptionsPost but the RequestParam has object 'FileuploadwithoptionsPostRequestOptions' is it because of this it cannot resolve the symbol.
#Parameter(name = "options", description = "") #Valid #RequestParam(value = "options", required = false) FileuploadwithoptionsPostRequestOptions options)
Is the object 'FileuploadwithoptionsPostRequestOptions' instantiated or does it have a similar class or interface matching the object present in #Request Param.
Ok, you have imported the required class for the object try using #RequestPart as you have used for file. Try if it works.
I'm trying to build a spring controller which essentially acts as a reverse-proxy for a geoserver instance.
For example if the client wants to access geoserver/wms?PARAM1=foo&PARAM2=bar, the controller will simply forward the request to the actual geoserver instance and serve back the response. In my case, geoserver either returns an XML payload or an image.
When testing this controller with an URL which returns an image, I am able to process the initial client request, forward it to geoserver and then process it but I'm getting the following error when serving the response to the client :
There was an unexpected error (type=Internal Server Error, status=500).
No converter for [class [B] with preset Content-Type 'image/png'
org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class [B] with preset Content-Type 'image/png'
Full stack trace
Here is the controller class:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
#Slf4j
#RestController
#BasePathAwareController
public class GeoServerProxyController {
//only used to check which converters are present
#Autowired
List<HttpMessageConverter<?>> converters;
#RequestMapping(value = "/geoserver/**")
#ResponseBody
public ResponseEntity<byte[]> forwardRequest(#RequestParam MultiValueMap<String, String> requestParams, #RequestHeader Map<String, String> headers, HttpServletRequest request) {
WebClient client = WebClient.builder()
.baseUrl("http://127.0.0.1:8090/geoserver/")
.build();
String url = new AntPathMatcher().extractPathWithinPattern("/geoserver/**", request.getRequestURI());
WebClient.ResponseSpec response = client.get()
.uri(uriBuilder -> uriBuilder
.path(url)
.queryParams(requestParams)
.build())
.headers(httpHeaders -> {
headers.forEach(httpHeaders::set);
})
.retrieve();
HttpHeaders responseHeaders = response.toBodilessEntity().map(HttpEntity::getHeaders).block();
byte[] responseBody = response.bodyToMono(byte[].class).block();
return ResponseEntity.ok().headers(responseHeaders).body(responseBody);
}
As advised in another thread, I have tried registering a byte array http message converter, which I was able to confirm is added to the list of http message converters.
#Configuration
public class WebMVCConfig implements WebMvcConfigurer {
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
final ByteArrayHttpMessageConverter arrayHttpMessageConverter = new ByteArrayHttpMessageConverter();
final List<MediaType> list = new ArrayList<>();
list.add(MediaType.IMAGE_JPEG);
list.add(MediaType.IMAGE_PNG);
list.add(MediaType.APPLICATION_OCTET_STREAM);
arrayHttpMessageConverter.setSupportedMediaTypes(list);
converters.add(arrayHttpMessageConverter);
}
}
This resulted in the same error.
I have also tried using InputStreamResource as a return type, as recommended by this article. It resulted in the same kind of error except with InputStreamResource instead of class [B].
I have also tried adding the following annotation to my controller method (which wasn't optimal as I would prefer not specifying a constant content type) :
#RequestMapping(value = "/geoserver/**", produces=MediaType.IMAGE_PNG_VALUE)
This also results in the exact same error.
I was not able to find a solution to this problem in other threads or in spring web documentation. The most common problem that somewhat resembles this deals with a "Content-Type=null" header, which is not my case.
Does anyone know how to solve this error ? Alternatively, is there a better way to serve distant image files through a Spring controller ?
The #ResponseBody annotation is used to serialize a Java object to the response (typically as JSON or XML).
Here you don't want to convert anything, just send raw binary content. Remove the annotation.
You should also change #RestController to #Controller, because #RestController automatically adds #ResponseBody.
It seems like the problem you are facing is due to #BasePathAwareController , remove it and you are good to go.
Description:
Based on the stack trace shared:
it occurs that SpringDataRest is trying to map an appropriate object to the content-type which it cannot find. hence throwing an generic 500 exception.
For those who are interested: i've had same issue and solve it like this.
This code is not the final, only for test in my case.
Angular frontend openlayers
Tile layer : i defined custom tileLoadFunction (example can be found in openlayers docs), url call my springboot backend api witch is proxying geoserver backend, a jwt token is sent in the fetch header (for security purpose, not mandatory)
new TileLayer({
source: new TileWMS({
url: 'http://localhost:8080/api/v1/map/wms',
tileLoadFunction: (tile, src) => {
const retryCodes = [408, 429, 500, 502, 503, 504];
const retries: any = {};
const image: any = (tile as ImageTile).getImage();
fetch(src, {
headers: {
Authorization: 'Bearer ' + this.oauthService.getAccessToken(),
},
})
.then((response) => {
if (retryCodes.includes(response.status)) {
retries[src] = (retries[src] || 0) + 1;
if (retries[src] <= 3) {
setTimeout(() => tile.load(), retries[src] * 1000);
}
return Promise.reject();
}
return response.blob();
})
.then((blob) => {
const imageUrl = URL.createObjectURL(blob);
image.src = imageUrl;
setTimeout(() => URL.revokeObjectURL(imageUrl), 5000);
})
.catch(() => tile.setState(3)); // error
},
params: {
LAYERS: 'TEST:section_33',
TILED: true,
},
serverType: 'geoserver',
transition: 0,
}),
Springboot backend
I have a RestController endpoint witch is close to the first post, and everythind is ok for me : wms geoserver tiles are called by my frontend, the bakend intercept these calls and make geoserver request and finally send back the tiles to the frontend and the wms layer appears to the map
#RestController
#RequestMapping("/api/v1/map")
#Slf4j
public class MapController {
#GetMapping(value = "/wms")
public ResponseEntity<byte[]> getWmsTile(#RequestParam MultiValueMap<String, String> requestParams, #RequestHeader Map<String, String> headers, HttpServletRequest request) throws IOException {
WebClient client = WebClient.builder()
.baseUrl("http://localhost:7070/geoserver/TEST/wms")
.build();
WebClient.ResponseSpec response = client.get()
.uri(uriBuilder -> uriBuilder
.path("")
.queryParams(requestParams)
.build())
.headers(httpHeaders -> {
headers.forEach(httpHeaders::set);
})
.retrieve();
HttpHeaders responseHeaders = response.toBodilessEntity().map(HttpEntity::getHeaders).block();
byte[] responseBody = response.bodyToMono(byte[].class).block();
return ResponseEntity.ok().contentType(MediaType.IMAGE_PNG).body(responseBody);
}
}
I have seen this issue being addressed in many other posts but none of them has solved my problem. I have a front-end Vue.js application and a spring boot Java application.
I am using the vue-google-oauth to prompt the Google sign in from my front end application to get the auth code, then I wanted to use my backend server to get user details and handle logic there.
On Google Cloud Platform I defined an Authorized redirect URI:
and I am using this very same uri when I am sending my auth code in the front end
import api from "#/assets/js/api";
import AdminNavigation from "./AdminNavigation";
import { mapGetters } from "vuex";
import Axios from "axios";
export default {
name: "Dashboard",
computed: {
...mapGetters(["IsSignedIn"]),
},
data() {
return {
title: "Christopher s' portfolio admin",
appDescription:
"Here you can add contents for the front end portfolio website.",
isInit: false,
};
},
components: {
AdminNavigation,
},
methods: {
signIn: async function () {
try {
const authCode = await this.$gAuth.getAuthCode();
Axios.post("http://localhost:8080/authenticate", {
code: authCode,
redirect_uri: "http://localhost:3000/admin/dashboard",
});
} catch (err) {
console.log(err);
}
},
},
mounted() {
let that = this;
let checkGauthLoad = setInterval(function () {
that.isInit = that.$gAuth.isInit;
if (!this.IsSignedIn) {
that.signIn();
}
if (that.isInit) clearInterval(checkGauthLoad);
}, 1000);
},
};
My backend server receives the auth code and the redirect_uri which is identical to what was defined on Google Cloud Platform.
package com.salay.christophersalayportfolio.controllers;
import com.google.api.client.auth.oauth2.TokenResponseException;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.salay.christophersalayportfolio.general.ConstantVariables;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
#Controller
public class AdminController {
#CrossOrigin(origins = "http://localhost:3000")
#RequestMapping(value = "/authenticate", method = RequestMethod.POST, consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
#ResponseBody
public String authentication(HttpEntity<String> data) throws IOException, ParseException {
JSONParser parser = new JSONParser();
JSONObject json = (JSONObject) parser.parse(data.getBody());
String authCode = json.get("code").toString();
String redirect_uri = json.get("redirect_uri").toString();
try {
GoogleTokenResponse response =
new GoogleAuthorizationCodeTokenRequest(new NetHttpTransport(), new JacksonFactory(),
ConstantVariables.GOOGLE_CLIENT_ID,
ConstantVariables.GOOGLE_CLIENT_SECRET,
authCode, redirect_uri).execute();
System.out.println("Access token: " + response.getAccessToken());
} catch (TokenResponseException e) {
if (e.getDetails() != null) {
System.err.println("Error: " + e.getDetails().getError());
if (e.getDetails().getErrorDescription() != null) {
System.err.println(e.getDetails().getErrorDescription());
}
if (e.getDetails().getErrorUri() != null) {
System.err.println(e.getDetails().getErrorUri());
}
} else {
System.err.println(e.getMessage());
}
}
return "";
}
}
But I get the following error:
400 Bad Request redirect_uri_mismatch
I kept looking at a lot of stack overflow questions and no solution worked for me so far... any ideas?
Sounds like you are not sending the OAuth details you think you are. Have you captured HTTPS messages to the Authorization Server from your Spring Boot back end - and can you post details here?
If it helps, this blog post of mine includes some notes on configuring an HTTP proxy in Java.
Say I have this resource:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresRoles;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
#Path("/authhello")
#Api(value = "hello", description = "Simple endpoints for testing api authentification",
hidden = true)
#Produces(MediaType.APPLICATION_JSON)
#RequiresAuthentication
public class AuthenticatedHelloWorldResource {
private static final String READ = "READ";
private static final String WRITE = "WRITE";
#GET
#ApiOperation(value = "helloworld",
notes = "Simple hello world.",
response = String.class)
#RequiresRoles(READ)
public Response helloWorld() {
String hello = "Hello world!";
return Response.status(Response.Status.OK).entity(hello).build();
}
#GET
#Path("/{param}")
#ApiOperation(value = "helloReply",
notes = "Returns Hello you! and {param}",
response = String.class)
#RequiresRoles(WRITE)
public Response getMsg(#PathParam("param") String msg) {
String output = "Hello you! " + msg;
return Response.status(Response.Status.OK).entity(output).build();
}
}
Should I write tests that confirm that certain (test) users get a response from the endpoints, and certain users don't? And if so: How can I write those tests? I've tried something like this:
import javax.ws.rs.core.Application;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Test;
import com.cognite.api.shiro.AbstractShiroTest;
import static org.junit.Assert.assertEquals;
public class AuthenticatedHelloWorldTest extends AbstractShiroTest {
#Override
protected Application configure() {
return new ResourceConfig(AuthenticatedHelloWorldResource.class);
}
#Test
public void testAuthenticatedReadHelloWorld() {
final String hello = target("/authhello").request().get(String.class);
assertEquals("Hello world!", hello);
}
#Test
public void testAuthenticatedWriteHelloWorld() {
final String hello = target("/authhello/test").request().get(String.class);
assertEquals("Hello you! test", hello);
}
}
but I'm not sure how to actually test the function of the #RequiresRoles-annotation. I've read Shiro's page on testing, but I haven't been able to write a failing test (e.g. a test for a subject that does not have the WRITE role trying to access /authhello/test). Any tips would be appreciated.
Should I even test this?
Yes. Provided you want to make sure that certain roles will have or have not access to your resource. This will be a security integration test.
How should I go about setting up the whole application + actually call it with an http request in a test if I am to test it? Or is there a simpler way?
Part of the issue is that #RequiresAuthentication and #RequiresRoles themselves are just class and method meta information. Annotations themselves do not provide the security check functionality.
It is not clear from your question what type of container you are using but I can guess that it is plain Jersey JAX-RS service (am I right?). For Shiro to perform security checks you should have added some JAX-RS filter (maybe some other way?) around your endpoints. To test security you should replicate this setup in your tests. Otherwise there is no engine processing your annotations and no security checks as the result.