Modern Spring Boot v3 with Thymeleaf, TailwindCSS and AlpineJS
In this tutorial, we will integrate Spring MVC with gulp and webpack. As you know creating Spring MVC project with Thymelaef project is so easy. But …
Integrating Google reCAPTCHA v3 with spring boot applications can greatly enhance security and protect against malicious activities, such as spam and bots. In this tutorial, we will learn how to integrate Google reCAPTCHA v3 with a Spring Boot backend and a Nuxt 3 frontend. By the end of this tutorial, you will be able to add reCAPTCHA v3 to your spring boot application and validate user interactions on both the server and client sides.
If you only need to see the code, here is the github link
Before diving into the code section, let’s take a look at the “what do we mean by reCAPTCHA?”
reCAPTCHA protects your website from fraud and abuse without creating friction.It uses adaptive challenges to keep malicious software from engaging in abusive activities on your website. Meanwhile, legitimate users will be able to login, make purchases, view pages, or create accounts and fake users will be blocked.
In short, reCAPTCHA’s purpose is to block malicious request from your application.
For instance sending 100.000 login request in 1 min (or even 10 min) can be malicious request
reCaptcha V2 comes in two different flavors:
reCAPTCHA v3 returns a score for each request without user friction. The score is based on interactions with your site and enables you to take an appropriate action for your site
The score ranges from 0.0 to 1.0. If score is 1.0, then means that request send by legitimate user. 0.0 means no human can act like that. And everything in between is a suspicion range.
I am not going into detail of how to setup Google reCAPTCHA in google console. Because Google can change the way you setup reCAPTCHA over the years. In this tutorial i am assuming that you already have Site Key and Secret Key. These keys will be given after you setup recaptcha in google console.
And don’t forget to set domain for localhost development. You should add at least two domains: localhost & 127.0.0.1
Let me summarize the reCAPTCHA flow for the login process:
I am assuming that you have already created spring boot project with the following dependency: Spring Web, Spring Security & Lombok
First, create the following object to send request between layers (from filter to service and more). Don’t forget to set secret key in the constructor
@Getter
@Setter
public class GoogleRecaptchaRequest {
private String secret;
private String response;
private String remoteip;
public GoogleRecaptchaRequest() {
this.secret = // secret key for recaptcha
}
}
After sending the request to the google, google will return the response (with includes many fields). We can wrap these fields into a class
To get more information about api response from the Google, checkout https://developers.google.com/recaptcha/docs/verify
@Data
@ToString
public class GoogleRecaptchaResponse {
private boolean success;
private String challenge_ts;
private String hostname;
@JsonProperty("error-codes")
private String[] errorCodes;
private Double score;
private String action;
}
Because we are going to send request to google, we need to configure the restTemplate to use anywhere in the spring context:
@Configuration
public class RestTemplateConfiguration {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
Create GoogleRecaptchaService
which has only one method verifyRecaptcha(request)
:
@Service
@Slf4j
@RequiredArgsConstructor
public class GoogleRecaptchaService {
// if score is lower than 0.5, means that request is malicious
// threshold value is up to you and can change according to the your business logic
private static final double SCORE_THRESHOLD = 0.5;
private static final String RECAPTCHA_VERIFY_ADDRESS = "https://www.google.com/recaptcha/api/siteverify";
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
public GoogleRecaptchaResponse verifyRecaptcha(GoogleRecaptchaRequest request) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("secret", request.getSecret());
map.add("response", request.getResponse());
map.add("remoteip", request.getRemoteip());
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
ResponseEntity<GoogleRecaptchaResponse> response = restTemplate.exchange(RECAPTCHA_VERIFY_ADDRESS,
HttpMethod.POST,
entity,
GoogleRecaptchaResponse.class);
GoogleRecaptchaResponse googleResponse = response.getBody();
if (Objects.isNull(googleResponse)) {
throw new RuntimeException("Google response is null");
}
if (Objects.nonNull(googleResponse.getScore()) && googleResponse.getScore() < SCORE_THRESHOLD) {
log.warn("User score is lower than threshold. GoogleResponse :: {}", googleResponse);
googleResponse.setSuccess(false);
}
return googleResponse;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
OncePerRequestFilter
class, which is a base class for filters that are only supposed to be applied once per request.HandlerExceptionResolver
, to resolve exceptions that occur during the filter execution.resolveException
method of the resolver instance to handle the exception and generate an appropriate response./**
* GoogleRecaptchaFilter class is responsible for filtering requests,
* extracting the reCAPTCHA response token from the request header,
* verifying the reCAPTCHA response using the googleRecaptchaService,
* and either allowing the request to proceed
* or generating an error response based on the verification result.
*/
@RequiredArgsConstructor
@Slf4j
public class GoogleRecaptchaFilter extends OncePerRequestFilter {
private final GoogleRecaptchaService googleRecaptchaService;
private final HandlerExceptionResolver handlerExceptionResolver;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String googleResponse = request.getHeader("google-recaptcha-token");
if (StringUtils.isBlank(googleResponse)) {
log.warn("Google response is null for the request :: {}", request.getRequestURL());
}
GoogleRecaptchaRequest googleRecaptchaRequest = new GoogleRecaptchaRequest();
googleRecaptchaRequest.setResponse(googleResponse);
googleRecaptchaRequest.setRemoteip(request.getRemoteAddr());
GoogleRecaptchaResponse googleRecaptchaResponse = googleRecaptchaService.verifyRecaptcha(googleRecaptchaRequest);
if (!googleRecaptchaResponse.isSuccess()) {
log.error("Google response is not success :: {}", googleRecaptchaResponse);
throw new RuntimeException("We have detected unusual activities from your browser. Please login again after a few minutes later");
}
filterChain.doFilter(request, response);
} catch (Exception ex) {
handlerExceptionResolver.resolveException(request, response, null, ex);
}
}
/**
* shouldNotFilter checks if the request URL contains the string "login"
* and returns **false** if it does.
* This means that the filter will be run for requests related to the login process.
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return !request.getRequestURL().toString().contains("login");
}
}
To register the filter as a bean, create an configuration class and define GoogleRecaptchaFilter
as a bean:
@Configuration
@RequiredArgsConstructor
public class AppConfiguration {
private final GoogleRecaptchaService googleRecaptchaService;
private final HandlerExceptionResolver handlerExceptionResolver;
@Bean
public GoogleRecaptchaFilter googleRecaptchaFilter() {
return new GoogleRecaptchaFilter(googleRecaptchaService, handlerExceptionResolver);
}
}
For the demo purpose, LoginController will be too simple, it will just return dummy string indicates that resource has definitely been called.
@RestController
public class LoginController {
@PostMapping(value = "/api/login")
public String doLogin() {
return "Login method has been called";
}
}
If you send the following curl request (request doesn’t include recatpcha token):
$ curl -X POST http://localhost:8080/api/login -H "Content-Type: application/json"
# response
We have detected unusual activities from your browser. Please login again after a few minutes later
In the log, you can see the response from the google:
ERROR c.m.r.filter.GoogleRecaptchaFilter : Google response is not success :: GoogleRecaptchaResponse(success=false, challenge_ts=null, hostname=null, errorCodes=[invalid-input-response], score=null, action=null)
If you send the following curl request (request includes dummy token value), then you will get the same response:
$ curl -X POST http://localhost:8080/api/login -H "Content-Type: application/json" -H "google-recaptcha-token: 1234"
# response
We have detected unusual activities from your browser. Please login again after a few minutes later
ERROR 243011 c.m.r.filter.GoogleRecaptchaFilter : Google response is not success :: GoogleRecaptchaResponse(success=false, challenge_ts=null, hostname=null, errorCodes=[invalid-input-response], score=null, action=null)
After you initialize your nuxt 3 project, create file called .env
and update it:
NUXT_PUBLIC_GOOGLE_RECAPTCHA_KEY= // recaptcha site key
And also update the nuxt.config.ts
file to read property inside the .env
export default defineNuxtConfig({
runtimeConfig: {
public: {
googleRecaptchaKey: process.env.NUXT_PUBLIC_GOOGLE_RECAPTCHA_KEY,
},
},
});
After these setup, we can read the siteKey using useRuntimeConfig()
It is a package for a simple and easy to use reCAPTCHA (v3 only) for Vue .
For more information https://www.npmjs.com/package/vue-recaptcha-v3
Install the package:
npm i vue-recaptcha-v3
In order to use vue-recaptcha-v3 inside the Nuxt 3 application, we need to register it as plugin.
Nuxt automatically reads the files in your plugins directory and loads them at the creation of the Vue application.
plugins
recaptcha.ts
:import { VueReCaptcha } from "vue-recaptcha-v3";
import { IReCaptchaOptions } from "vue-recaptcha-v3/dist/IReCaptchaOptions";
// The plugin enables the usage of Google reCAPTCHA in a Nuxt.js application
// by registering the VueReCaptcha plugin with the necessary configuration options.
export default defineNuxtPlugin((nuxtApp) => {
// The useRuntimeConfig function is called to retrieve the runtime
// configuration of the Nuxt.js application.
const config = useRuntimeConfig();
const options: IReCaptchaOptions = {
siteKey: config.public.googleRecaptchaKey,
loaderOptions: {
useRecaptchaNet: true,
},
};
nuxtApp.vueApp.use(VueReCaptcha, options);
});
In order to use recaptcha operation across all pages, create the composables/useGoogleRecaptcha.ts
file
import { useReCaptcha } from "vue-recaptcha-v3";
export class RecaptchaAction {
public static readonly login = new RecaptchaAction("login");
private constructor(public readonly name: string) {}
}
/**
* The exported executeRecaptcha function allows
* you to execute reCAPTCHA actions
* and retrieve the reCAPTCHA token along with the header options
* to be used in subsequent requests.
*/
export default () => {
let recaptchaInstance = useReCaptcha();
const executeRecaptcha = async (action: RecaptchaAction) => {
/**
* Wait for the recaptchaInstance to be loaded
* by calling the recaptchaLoaded method.
* This ensures that the reCAPTCHA library is fully loaded
* and ready to execute reCAPTCHA actions.
*/
await recaptchaInstance?.recaptchaLoaded();
const token = await recaptchaInstance?.executeRecaptcha(action.name);
const headerOptions = {
headers: {
"google-recaptcha-token": token,
},
};
return { token, headerOptions };
};
return { executeRecaptcha };
};
Create pages/index.vue
file, we will add the form login in here.
<template>
<div class="form-center">
<form @submit.prevent="handleSubmit">
<label>Email:</label>
<input type="email" name="email" placeholder="Email" />
<input type="submit" />
</form>
</div>
</template>
<script setup lang="ts">
import useGoogleRecaptcha, {
RecaptchaAction,
} from "~/composables/useGoogleRecaptcha";
const { executeRecaptcha } = useGoogleRecaptcha();
const handleSubmit = async () => {
const { token } = await executeRecaptcha(RecaptchaAction.login);
const fetchData = await $fetch<string>("/api/login", {
baseURL: "http://localhost:8080",
method: "POST",
headers: {
"Content-Type": "application/json",
"google-recaptcha-token": token ?? "",
},
});
console.log("response from api :: ", fetchData);
};
</script>
<style scoped>
.form-center {
width: 400px;
margin: 0 auto;
}
</style>
In this page:
Here is the sample response from the backend after form is submitted:
GoogleRecaptchaResponse(success=true, challenge_ts=2023-05-18T11:46:28Z, hostname=localhost, errorCodes=null, score=0.9, action=login)
Integrating Google reCAPTCHA v3 with Spring Boot and Nuxt 3 offers several benefits for developers aiming to enhance the security and protection of their web applications:
In this tutorial, we will integrate Spring MVC with gulp and webpack. As you know creating Spring MVC project with Thymelaef project is so easy. But …
Let’s say you have two entities which has many-to-one relationship and you want to paginate your query on the parent side with additional colums …