This tutorial shows you how to create a slick-looking, full-stack, secure application using React, Spring Boot, and JHipster.
Prerequisites:
I recommend using SDKMAN to manage your OpenJDK installations. Run sdk install java 11.0.2-open to install Java 11 and sdk install java 17.0.1 for Java 17.
If you’re on Windows, you may need to install the Windows Subsystem for Linux for some commands to work.
|
Tip
|
The brackets at the end of some steps indicate the IntelliJ Live Templates to use. You can find the template definitions at mraible/idea-live-templates. You can also find the source code in the blog post and GitHub repo. |
- Full Stack Development with React and Spring Boot
- Get Started with JHipster 7
- Change Your Identity Provider to Auth0
- Test Your Full Stack Java App with Cypress
- Create Entities to allow CRUD on Photos
- Add Image EXIF Processing in Your Spring Boot API
- Add a React Photo Gallery
- Make Your Full Stack Java App Into a PWA
- Deploy Your React + Spring Boot App to Heroku
- Get Hip with JHipster!
Normally, you use Create React App and Spring Initializr.
Today, I’ll show you how to build a Flickr clone with React and Spring Boot. However, I’m going to cheat. Rather than building everything using the aforementioned tools, I’m going to use JHipster.
JHipster is an application generator that initially only supported Angular and Spring Boot. Now it supports Angular, React, and Vue for the frontend. JHipster also has support for Kotlin, Micronaut, Quarkus, .NET, and Node.js on the backend.
Why React? Because it’s currently the most popular. Angular and Vue will work too though.
-
Run the following command to install JHipster:
npm i -g generator-jhipster@7
-
To create a full-stack app with JHipster, create a directory, and run
jhipsterin it:mkdir full-stack-java cd full-stack-java jhipster
-
Choose the defaults except for:
-
name:
flickr2 -
package:
com.auth0.flickr2 -
authentication:
OAuth 2.0 / OIDC -
client:
React -
bootswatch:
United > Dark -
testing:
Cypress
-
-
Start Keycloak with the following command in your project’s root directory.
docker-compose -f src/main/docker/keycloak.yml up -d # or use jhkeycloakup from https://www.jhipster.tech/oh-my-zsh/
-
Verify everything works:
./mvnw
-
Open another terminal and run Cypress tests:
npm run e2e
-
✅ All tests should pass!
To switch from Keycloak to Auth0, you just need to override the default properties for Spring Security OAuth.
Create a .auth0.env file in the root of your project, and fill it with the code below to override the default OIDC settings:
export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=https://<your-auth0-domain>/ export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=<your-client-id> export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=<your-client-secret> export JHIPSTER_SECURITY_OAUTH2_AUDIENCE=https://<your-auth0-domain>/api/v2/
|
Warning
|
Modify your existing .gitignore file to have *.env so you don’t accidentally check in your secrets! |
-
Log in to your Auth0 account (or sign up if you don’t have an account).
-
Press the Create Application button in the Applications section. Use a name like
JHipster Baby!, select Regular Web Applications, and click Create. -
Switch to the Settings tab and configure your application settings:
-
Allowed Callback URLs:
http://localhost:8080/login/oauth2/code/oidc -
Allowed Logout URLs:
http://localhost:8080/
-
-
Scroll to the bottom and click Save Changes.
-
Replace the
$VARIABLESin.auth0.envwith the settings from your app:export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=https://$AUTH0_DOMAIN/ export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=$CLIENT_ID export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=$CLIENT_SECRET export JHIPSTER_SECURITY_OAUTH2_AUDIENCE=https://$AUTH0_DOMAIN/api/v2/
-
In the roles section, create new roles named
ROLE_ADMINandROLE_USER. -
Create a new user account in the users section.
-
Click on the Role tab to assign the roles you just created to the new account.
NoteMake sure your new user’s email is verified before attempting to log in! -
Next, head to Auth Pipeline > Rules > Create. Select the
Empty ruletemplate. Provide a meaningful name likeGroup claimsand replace the Script content with the following.function(user, context, callback) { user.preferred_username = user.email; const roles = (context.authorization || {}).roles; function prepareCustomClaimKey(claim) { return `https://www.jhipster.tech/${claim}`; } const rolesClaim = prepareCustomClaimKey('roles'); if (context.idToken) { context.idToken[rolesClaim] = roles; } if (context.accessToken) { context.accessToken[rolesClaim] = roles; } callback(null, user, context); }
This code is adding the user’s roles to a custom claim. This claim is mapped to Spring Security authorities in
SecurityUtils.java.The
SecurityConfiguration.javaclass has aGrantedAuthoritiesMapperbean that calls this method to configure a user’s roles from their OIDC data. -
Click Save changes to continue.
Want to have all these steps automated for you? Vote for issue #351 in the Auth0 CLI project. What about Okta? You can use it too! See JHipster’s documentation.
-
Stop your JHipster app using Ctrl+C, set your Auth0 properties in
.auth0.env, and start your app again.source .auth0.env ./mvnw
-
Voilà - your app is now using Auth0! Open your favorite browser to
http://localhost:8080. -
Log in and show everything working.
JHipster has Auth0 support built-in, so you can specify your credentials for Cypress tests and automate your UI testing!
-
Open a new terminal window and specify the credentials for the Auth0 user you just created.
export CYPRESS_E2E_USERNAME=<new-username> export CYPRESS_E2E_PASSWORD=<new-password>
-
Then, run all the end-to-end tests.
npm run e2e
CautionIf you experience authentication errors when running Cypress tests, it’s likely because you’ve violated Auth0’s Rate Limit Policy. As a workaround, I recommend you use Keycloak for end-to-end tests. You can do this by opening a new terminal window and starting your app there using ./mvnw. Then, open a second terminal window and runnpm run e2e.
Now, let’s create some data handling for this Flickr clone!
JHipster has a JDL (JHipster Domain Language) feature that allows you to model the data in your app, and generate entities from it. You can use the JDL Studio to do this online and save it locally once you’ve finished.
-
Copy the JDL below and save it in a
flickr2.jdlfile in the root directory of your project.entity Album { title String required description TextBlob created Instant } entity Photo { title String required description TextBlob image ImageBlob required height Integer width Integer taken Instant uploaded Instant } entity Tag { name String required minlength(2) } relationship ManyToOne { Album{user(login)} to User Photo{album(title)} to Album } relationship ManyToMany { Photo{tag(name)} to Tag{photo} } paginate Album with pagination paginate Photo, Tag with infinite-scroll -
Generate entities and CRUD code (Java for Spring Boot; TypeScript and JSX for React) by importing the JDL:
jhipster jdl flickr2.jdl
This process will create Liquibase changelog files, entities, repositories, Spring MVC controllers, and all the React code necessary to create, read, update, and delete your entities. It’ll even generate JUnit unit tests, Jest unit tests, and Cypress end-to-end tests!
-
After the process completes, you can restart your app, log in, and browse through the Entities menu. Try adding some data to confirm everything works.
The Photo entity has a few properties that can be calculated by reading the uploaded photo’s EXIF (Exchangeable Image File Format) data.
-
Add a dependency on Drew Noakes' metadata-extractor library to your
pom.xml:<dependency> <groupId>com.drewnoakes</groupId> <artifactId>metadata-extractor</artifactId> <version>2.16.0</version> </dependency>
-
Then modify the
PhotoResource#createPhoto()method to set the metadata when an image is uploaded. [java-metadata]public class PhotoResource { ... public ResponseEntity<Photo> createPhoto(@Valid @RequestBody Photo photo) { ... try { photo = setMetadata(photo); } catch (ImageProcessingException ipe) { log.error(ipe.getMessage()); } Photo result = photoRepository.save(photo); ... } }
Since you’re extracting the information, you can remove the fields from the UI and tests so the user cannot set these values.
-
In
photo-update.tsx, hide the metadata so users can’t edit it. Rather than displaying theheight,width,taken, anduploadedvalues, hide them. You can do this by searching forphoto-height, grabbing the elements (and its following three elements) and adding them to ametadataconstant just afterdefaultValues()lambda function.const defaultValues = () => ... const metadata = ( <div> <ValidatedField label={translate('flickr2App.photo.height')} id="photo-height" name="height" data-cy="height" type="text" /> <ValidatedField label={translate('flickr2App.photo.width')} id="photo-width" name="width" data-cy="width" type="text" /> <ValidatedField label={translate('flickr2App.photo.taken')} id="photo-taken" name="taken" data-cy="taken" type="datetime-local" placeholder="YYYY-MM-DD HH:mm" /> <ValidatedField label={translate('flickr2App.photo.uploaded')} id="photo-uploaded" name="uploaded" data-cy="uploaded" type="datetime-local" placeholder="YYYY-MM-DD HH:mm" /> </div> ); const metadataRows = isNew ? '' : metadata; return ( ... );
-
Then, in the
returnblock, remove the JSX between theimageproperty andalbumproperty and replace it with{metadataRows}.<ValidatedBlobField label={translate('flickr2App.photo.image')} ... /> {metadataRows} <ValidatedField id="photo-album" name="albumId" ...> ... </ValidatedField>
-
In
photo.spec.ts, remove the code that sets the data in these fields:cy.get(`[data-cy="height"]`).type('99459').should('have.value', '99459'); cy.get(`[data-cy="width"]`).type('61514').should('have.value', '61514'); cy.get(`[data-cy="taken"]`).type('2021-10-11T16:46').should('have.value', '2021-10-11T16:46'); cy.get(`[data-cy="uploaded"]`).type('2021-10-11T15:23').should('have.value', '2021-10-11T15:23');
-
Stop your Maven process, run
source .auth0.env, then./mvnwagain. -
If you upload an image you took with your smartphone, the height, width, and taken values should all be populated. If they’re not, chances are your image doesn’t have the data in it.
|
Note
|
Need some sample photos with EXIF data? You can download pictures of my 1966 VW Bus from an album on Flickr. |
You’ve added metadata extraction to your backend, but your photos still display in a list rather than in a grid (like Flickr). To fix that, you can use the React Photo Gallery component.
-
Install it using npm:
npm i react-photo-gallery@8 --force
-
In
photo.tsx, add an import forGallery:import Gallery from 'react-photo-gallery';
-
Then add the following just after
const { match } = props;. This adds the photos to a set with source, height, and width information.const photoSet = photoList.map(photo => ({ src: `data:${photo.imageContentType};base64,${photo.image}`, width: photo.height > photo.width ? 3 : photo.height === photo.width ? 1 : 4, height: photo.height > photo.width ? 4 : photo.height === photo.width ? 1 : 3 }));
-
Next, add a
<Gallery>component right after the closing</h2>.return ( <div> <h2 id="photo-heading" data-cy="PhotoHeading"> ... </h2> <Gallery photos={photoSet} /> ... );
-
Save all your changes and restart your app.
source .auth0.env ./mvnw
-
Log in and navigate to Entities > Photo in the top navbar. You will see a plethora of photos loaded by Liquibase and faker.js. To make a clean screenshot without this data, modify
application-dev.ymland remove the "faker" context for Liquibase.liquibase: # Append ', faker' to the line below if you want sample data to be loaded automatically contexts: dev
-
Stop your Spring Boot backend and run
rm -r target/h2dbto clear out your database. Restart your backend. -
Now you should be able to upload photos and see the results in a nice grid at the top of the list.
|
Tip
|
You can also add a "lightbox" feature to the grid so you can click on photos and zoom in. The React Photo Gallery docs shows how to do this. I’ve integrated it into the example for this post, but I won’t show the code here for the sake of brevity. You can see the (final photo.tsx with Lightbox added on GitHub or a diff of the necessary changes.
|
To make a web app into a PWA:
-
Your app must be served over HTTPS
-
Your app must register a service worker so it can cache requests and work offline
-
Your app must have a webapp manifest with installation information and icons
For HTTPS, you can set up a certificate for localhost or (even better), deploy it to production! Cloud providers like Heroku will provide you with HTTPS out-of-the-box, but they won’t force HTTPS.
-
To force HTTPS, open
SecurityConfiguration.javaand add a rule to force a secure channel when anX-Forwarded-Protoheader is sent.@Override protected void configure(HttpSecurity http) throws Exception { http ... .and() .frameOptions() .deny() .and() .requiresChannel() .requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null) .requiresSecure() .and() .authorizeRequests() ... }
-
To register a service worker, open
src/main/webapp/index.htmland uncomment the following block of code.<script> if ('serviceWorker' in navigator) { window.addEventListener('load', function () { navigator.serviceWorker.register('/service-worker.js').then(function () { console.log('Service Worker Registered'); }); }); } </script>
-
The final feature — a webapp manifest — is included at
src/main/webapp/manifest.webapp. It defines an app name, colors, and icons. You might want to adjust these to fit your app.
-
To deploy your app to Heroku, you’ll first need to install the Heroku CLI. You can confirm it’s installed by running
heroku --version. If you don’t have a Heroku account, go to heroku.com and sign up. -
Run
heroku loginto log in to your account, then start the deployment process with JHipster:jhipster heroku
-
When prompted to use Okta for OIDC, select
No. -
You’ll be prompted to overwrite
pom.xml. Typeato allow overwriting all files.
If you have a stable and fast internet connection, your app should be live on the internet in a few minutes! 😀
|
Tip
|
You can watch the Double Rainbow video if you want a smile while you’re waiting. |
-
To configure your app to work with Auth0 on Heroku, set your environment variables:
AUTH0_DOMAIN=https://$YOUR_DOMAIN/ CLIENT_ID=$YOUR_CLIENT_ID CLIENT_SECRET=$YOUR_CLIENT_SECRET AUDIENCE=https://$AUTH0_DOMAIN/api/v2/
-
Run
heroku config:setto configure Auth0 as your identity provider:heroku config:set \ SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=$AUTH0_DOMAIN \ SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=$CLIENT_ID \ SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=$CLIENT_SECRET \ JHIPSTER_SECURITY_OAUTH2_AUDIENCE=$AUDIENCE
Use
heroku logs --tailto watch your logs. -
After Heroku restarts your app, open it with
heroku open. Copy its URL. -
Log in to your Auth0 account, navigate to your app, and add your Heroku URLs as valid redirect URIs:
-
Allowed Callback URLs:
https://flickr-2.herokuapp.com/login/oauth2/code/oidc -
Allowed Logout URLs:
https://flickr-2.herokuapp.com
-
-
Test it with Lighthouse or WebPageTest.
-
Confirm excellent security headers at securityheaders.com.
Wahoo! You streamlined your path to full-stack Java development with JHipster!! 👏👏👏
🤓 Find the code on GitHub: @oktadev/auth0-full-stack-java-example
👀 Read the blog post: Full Stack Java with React, Spring Boot, and JHipster