How to load dynamically uploaded images within a Vaadin/Spring Boot app?

Recently I was faced with the question on how to efficiently load images in a Vaadin Flow app, that can dynamically be changed by the users. As you might have suspected we are talking about simple user profile pictures. The images are supposed to be held private and only be available for logged-in users. Thus, the means which provide the images are supposed to be under control of the Vaadin session. For loading images Vaadin sources usually present code snippets like the following (Ref):

Avatar user = new Avatar(name);
StreamResource imageResource = new StreamResource("user-image.png",
        () -> getClass()
                .getResourceAsStream("/images/user-image.png"));
company.setImageResource(imageResource);

Unfortunately, this approach only works for static content on the applications’ classpath and not for dynamic content (Ref). We can further discover the provided methods of the Avatar object, and we will notice that it will also accept a simple URI for an image source:

Avatar user = new Avatar(name);
user.setImage(uri);

That seems logical to exist, but how do we now provide the URI and make sure it is not accessible by anyone not logged-in? For this the best solution seemed to be the creation of an additional REST endpoint that provides images as byte arrays and is under control by Vaadin via its influence over Spring Security.

Hence, we will create a Spring web controller with an endpoint such as:

@RestController
@RequestMapping(ProfilePictureController.PATH)
@AllArgsConstructor
@Tag(name = "Profile Pictures", description = "Endpoints to query for profile picture")
@Slf4j
@CrossOrigin
public class ProfilePictureController {

    public static final String PATH = "/v1/img";

    private final FileHandlingService fileHandlingService;

    @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    @ResponseStatus(HttpStatus.OK)
    @SneakyThrows
    public ResponseEntity<byte[]> getProfilePicture(@PathVariable("id") final UUID id) {
        try {
            final FileStorageDto fileStorageDto = this.fileHandlingService.findById(id);

            try (final InputStream inputStream = this.fileHandlingService.downloadFile(id)) {
                byte[] image = IOUtils.toByteArray(inputStream);
                final MediaType mediaType = MediaType.parseMediaType(fileStorageDto.getMimeType());
                return ResponseEntity.ok().contentType(mediaType).body(image);
            }

        } catch (SomeException pe) {
            return ResponseEntity.notFound().build();
        }
    }
}

The above snippet already contains a few additional ideas, such as the handling of mime types and the handling of unfound images based on a custom exception type. Apart from that it is a rather simple endpoint that tries to find the user image based on a provided id on the endpoint. The used fileHandlingService could be a dedicated service handling files on te file system or connect to a S3-compatible source. Additional caching could also be introduced either header-based or server-side.

In order for this endpoint to be reachable only for logged-in users we are actually not required to do anything else. The served images will by default be only available to logged-in users. If you want to exhibit more control over who gets what data such measures would need to be implemented additionally on the endpoint e.g. via the @PreAuthorize logic.

If the endpoint is supposed to be available also for not logged-in users the URL can be excluded from Vaadin default handling via a Spring Boot property:

vaadin.exclude-urls=/v1/img/**

This exclusion could also be directly configured by extending the VaadinWebSecurity class and overriding configure(final HttpSecurity http) with additional logic.

Finally, to provide a full qualified URL to the Avatar object we might want to supply protocol, host and port based on the environment to the application context and conveniently add e.g. the id of the image(s) to load.