Welcome, In this post, you will learn about how to implement Google reCAPTCHA v3 using Spring Webflux and Angular.
· Prerequisites
· Overview
∘ What is reCAPTCHA?
∘ Google reCAPTCHA type
· Why use reCaptcha v3
· Getting Started
∘ Setting up reCaptcha
∘ Client-side application with Angular
∘ Server-side application with Spring Webflux
· Project Structure
· Testing
· Conclusion
· References
Last updated: May 18, 2025
Prerequisites
This is the list of all the prerequisites for following this story:
- Node.js and npm
- Angular CLI
- Java 17+
- Spring Boot / WebFlux 3+
- MongoDB instance (v6 or later) installed
- Maven 3.6.3
- Google Account
- Basic knowledge of Angular and TypeScript (Angular 19 or later)
- IntelliJ IDEA, Visual Studio Code, or another IDE
Overview
What is reCAPTCHA?
reCAPTCHA is a free service from Google that helps protect websites from spam and abuse. A “CAPTCHA” is a turing test to tell human and bots apart. It is easy for humans to solve, but hard for “bots” and other malicious software to figure out. By adding reCAPTCHA to a site, you can block automated software while helping your welcome users to enter with ease.
Google reCAPTCHA type
There are four types of reCaptcha.
- reCaptcha v3: allows verifying if an interaction is legitimate without any user interaction. It is a pure JavaScript API returning a score, giving you the ability to take action in the context of your site.
- reCaptcha v2 “I’m not a robot” Checkbox: requires the user to click a checkbox indicating the user is not a robot. This will either pass the user immediately (with No CAPTCHA) or challenge them to validate whether or not they are human.
- Invisible reCaptcha v2: does not require the user to click on a checkbox, instead, it is invoked directly when the user clicks on an existing button on your site or can be invoked via a JavaScript API call.
- reCaptcha Android: It is an Android library that is part of the Google Play services SafetyNet APIs. This library provides native Android APIs that you can integrate directly into an app.

Why use reCaptcha v3
reCAPTCHA v3 is a new invisible security measure introduced by Google.
It returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot) for each user’s frictionless request. The score is based on interactions with your site and allows you to take an action appropriate for your site.
reCAPTCHA v3 introduces a new concept: actions. When you specify an action name in each place you execute reCAPTCHA, you enable the following new features:
- A detailed breakdown of data for your top ten actions in the admin console
- Adaptive risk analysis is based on the context of the action, because abusive behavior can vary.
Getting Started
Setting up reCAPTCHA
After you log in to your Google account, go to the Google reCAPTCHA v3 admin console.

Fill in the Label field, set the reCAPTCHA v3 option, add Localhost as domains (to test the app locally), agree to the reCAPTCHA Terms of Service, and click the Submit button.

Now, we have two (2) keys for client-side and server-side integration.
Client-side application with Angular
Let’s create the Angular project ng new angular-recaptcha-v3
Install the ng-recaptcha-2 package: npm install ng-recaptcha-2
To start, we need to import the RecaptchaV3Module and provide the reCAPTCHA v3 site key using RECAPTCHA_V3_SITE_KEY injection token in app.config.ts file.
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(),
provideRouter(routes),
// ✅ captcha module integration
importProvidersFrom(RecaptchaModule, RecaptchaV3Module),
{ provide: RECAPTCHA_V3_SITE_KEY, useValue: environment.recaptcha.siteKey },
],
};
We will add reCAPTCHA v3 on the registration page.
<div class="container">
<h1 class="text-center" style="padding: 50px;">Welcome - Google reCAPTCHA v3</h1>
<div class="card">
<h4 class="card-header">Register</h4>
<p class="text-center" style="color: red; padding: 10px; font-weight: bold;">{{errorResponse}}</p>
<div class="card-body">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" formControlName="firstName" class="form-control" [class.is-invalid]="submitted && f['firstName'].errors" />
@if (submitted && f['firstName'].errors) {
<div class="invalid-feedback">
@if (f['firstName'].errors['required']) {
<div>First Name is required</div>
}
</div>
}
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" formControlName="lastName" class="form-control" [class.is-invalid]="submitted && f['lastName'].errors" />
@if (submitted && f['lastName'].errors) {
<div class="invalid-feedback">
@if (f['lastName'].errors['required']) {
<div>Last Name is required</div>
}
</div>
}
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" formControlName="username" class="form-control" [class.is-invalid]="submitted && f['username'].errors" />
@if (submitted && f['username'].errors) {
<div class="invalid-feedback">
@if (f['username'].errors['required']) {
<div>Username is required</div>
}
</div>
}
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="text" formControlName="email" class="form-control" [class.is-invalid]="submitted && f['email'].errors" />
@if (submitted && f['email'].errors) {
<div class="invalid-feedback">
@if (f['email'].errors['required']) {
<div>Email is required</div>
}
@if (f['email'].errors['email']) {
<div>mail address is invalid</div>
}
</div>
}
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" formControlName="password" class="form-control" [class.is-invalid]="submitted && f['password'].errors" />
@if (submitted && f['password'].errors) {
<div class="invalid-feedback">
@if (f['password'].errors['required']) {
<div>Password is required</div>
}
@if (f['password'].errors['minlength']) {
<div>Password must be at least 6 characters</div>
}
</div>
}
</div>
<div class="form-group">
<label for="passwordConfirmation">Password Confirmation</label>
<input type="password" formControlName="passwordConfirmation" class="form-control" [class.is-invalid]="submitted && f['passwordConfirmation'].errors" />
@if (submitted && f['passwordConfirmation'].errors) {
<div class="invalid-feedback">
@if (f['passwordConfirmation'].errors['required']) {
<div>Password confirmation is required</div>
}
@if (f['passwordConfirmation'].errors['minlength']) {
<div>Password confirmation must be at least 6 characters</div>
}
</div>
}
</div>
<div class="form-group" style="margin-top: 20px;">
<button [disabled]="loading" class="btn btn-primary">
@if (loading) {
<span class="spinner-border spinner-border-sm mr-1"></span>
}
Register
</button>
</div>
</form>
</div>
</div>
</div>
Output

Our component file register.component.ts will look as follows:
@Component({
selector: 'app-register',
imports: [ReactiveFormsModule, CommonModule],
templateUrl: './register.component.html',
styleUrl: './register.component.scss',
})
export class RegisterComponent implements OnInit {
readonly formBuilder = inject(FormBuilder);
readonly route = inject(ActivatedRoute);
readonly router = inject(Router);
readonly accountService = inject(AccountService);
readonly recaptchaV3Service = inject(ReCaptchaV3Service);
form!: FormGroup;
loading = false;
submitted = false;
errorResponse: string;
constructor() {
// redirect to home if already registered
if (this.accountService.isUserAuthenticated()) {
this.router.navigate(['/home']);
}
this.errorResponse = '';
}
ngOnInit() {
this.initForm();
}
initForm(): void {
this.form = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
username: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
passwordConfirmation: [
'',
[Validators.required, Validators.minLength(6)],
],
});
}
// convenience getter for easy access to form fields
get f() {
return this.form.controls;
}
onSubmit() {
this.submitted = true;
// reset error message
this.errorResponse = '';
// check if form is valid
if (this.form.invalid) {
return;
}
this.loading = true;
this.recaptchaV3Service.execute('importantAction').subscribe((token) => {
this.accountService
.register(this.form.value, token)
.pipe(first())
.subscribe({
next: (response) => {
this.errorResponse = 'Registration successful';
localStorage.setItem('user', JSON.stringify(response.data));
this.router.navigate(['/home']);
},
error: (error: any) => {
var response = <ApiResponse>error.error;
this.errorResponse = response.message;
this.loading = false;
}
});
});
}
}
ReCaptchaV3Service is injected as a dependency, which will emit a token value for verification purposes on the backend. Once the response token is generated, we need to verify it within two minutes with reCAPTCHA using the Google API to make sure the token is valid.
Server-side application with Spring Webflux
We will start by creating a simple Spring reactive project from start.spring.io, with the following dependencies: Spring Reactive Web, Spring Security, Lombok, and Validation.

Project Structure
Our project structure will look like this:

We store Google reCAPTCHA keys in the application.yml:
google:
recaptcha:
secret: 6LflWVUhAAAAAOhh5gVMhHk66MrXXXXXXXXXXXX
url: https://www.google.com/recaptcha/api/siteverify
score-threshold: 0.5
/**
* google recaptcha configuration properties
*/
@ConfigurationProperties(prefix = "google.recaptcha")
public record CaptchaProperties(String secret, String url, double scoreThreshold) {
}
The next step is to verify the CAPTCHA when processing the user registration endpoint.
UserController class
/**
* REST controller for managing the current user's account.
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
private final CaptchaService captchaService;
/**
* {@code POST /register} : register the user.
*/
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public Mono<ApiResponseDTO> registerAccount(@Valid @RequestBody UserDTO userDTO) {
if (!isPasswordMatches(userDTO.getPassword(), userDTO.getPasswordConfirmation())) {
throw new InvalidPasswordException("Password not matches");
}
return captchaService.verify(userDTO.getCaptchaToken())
.flatMap(u -> userService.createUser(userDTO))
.map(r -> new ApiResponseDTO(r, "Registration successful"));
}
private static boolean isPasswordMatches(String password, String passwordConfirmation) {
return (StringUtils.hasText(StringUtils.trimAllWhitespace(password)) && StringUtils.hasText(StringUtils.trimAllWhitespace(passwordConfirmation))
&& password.equals(passwordConfirmation));
}
}
Our service class CaptchaServiceImpl, will be used to verify a user’s response to a reCAPTCHA challenge. Google provides an endpoint (URL: https://www.google.com/recaptcha/api/siteverify, METHOD: POST) to which we will POST to verify the captcha. Below is the code that verifies the captcha.
@Slf4j
@RequiredArgsConstructor
@Service
public class CaptchaServiceImpl implements CaptchaService {
public final CaptchaProperties captchaProperties;
private final WebClient.Builder webclientBuilder;
@Override
public Mono<RecaptchaResponse> verify(String tokenResponse) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("secret", captchaProperties.secret());
formData.add("response", tokenResponse);
return webclientBuilder.build().post()
.uri(captchaProperties.url())
.bodyValue(formData)
.retrieve()
.bodyToMono(RecaptchaResponse.class)
.doOnSuccess(recaptchaResponse -> {
log.info("response verify captcha: {}", recaptchaResponse.toString());
if (!recaptchaResponse.isSuccess()){
throw new InvalidCaptchaException("reCaptcha v3 was not successfully validated");
}
if(recaptchaResponse.getScore() < captchaProperties.scoreThreshold()){
throw new InvalidCaptchaException("Low score for reCaptcha v3");
}
})
.doOnError(e -> {
log.error("error verify captcha : {}", e.getMessage());
throw new InvalidCaptchaException(e.getMessage());
});
}
}
Testing
Run the backend and frontend applications


The score generated by the server is 0.9 We can validate the registration.
After successful registration, the user is redirected to the homepage

Based on the score, actions can be taken based on the use case of the site.

Conclusion
In this post, we implemented Google reCAPTCHA v3 using Spring Webflux and Angular 19.
The complete source code is available on GitHub.
Support me through GitHub Sponsors.