# Java bot protection for Spring Boot | Nolgard

> Verify Nolgard tokens from Java with java.net.http.HttpClient and Jackson. A Spring Boot @RestController that calls /v1/verify.

Source: https://nolgard.com/docs/java/
Last updated: 2026-10-25

[Docs](https://nolgard.com/docs/) / Java

Guide

# Java

A Spring Boot controller using the JDK's `HttpClient` and Jackson. No extra HTTP library.

## Browser

```
<script src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" defer></script>
<script>
  document.querySelector('#signup').addEventListener('submit', async (event) => {
    event.preventDefault();
    let token; // stays undefined when the script is blocked: the server takes the missing-token path
    try {
      await Nolgard.ready;
      token = await Nolgard.execute({ action: 'signup' });
    } catch {}
    const form = event.target;
    const response = await fetch('/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, email: form.elements.email.value })
    });
    // Show your own success or error message here.
  });
</script>
```

## Spring Boot

```
// SignupController.java
package com.example.signup;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SignupController {
    private static final URI VERIFY_URL = URI.create("https://nolgard.com/v1/verify");
    private static final Map<String, Object> UNAVAILABLE = Map.of("success", false, "error", "unavailable");
    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(3))
            .build();

    private final ObjectMapper json = new ObjectMapper();
    private final AccountService accounts; // your own bean: create(email, trialDays, confirmEmailFirst)

    public SignupController(AccountService accounts) {
        this.accounts = accounts;
    }

    public record SignupRequest(String token, String email) {}

    @PostMapping("/signup")
    public ResponseEntity<Map<String, Object>> signup(@RequestBody SignupRequest body, HttpServletRequest request) {
        String email = Objects.requireNonNullElse(body.email(), "");
        String token = Objects.requireNonNullElse(body.token(), "");
        Map<String, Object> result = nolgardVerify(token, "signup", request.getRemoteAddr(), Map.of("email", email));

        if (!Boolean.TRUE.equals(result.get("success"))) {
            String error = String.valueOf(result.get("error"));
            switch (error) {
                case "invalid-secret", "invalid-input" -> { // config error: fail closed
                    return ResponseEntity.status(503).body(Map.of("error", "try-again"));
                }
                case "missing-token" -> { // script blocked: not a bot verdict
                    accounts.create(email, 3, true);
                    return ResponseEntity.ok(Map.of("ok", true, "confirm_email", true));
                }
                case "unavailable", "over-quota" -> { // fail open with a reduced grant
                    accounts.create(email, 3, false);
                    return ResponseEntity.ok(Map.of("ok", true));
                }
                default -> { // stale token: the form retries once
                    return ResponseEntity.status(409).body(Map.of("error", "retry-token"));
                }
            }
        }

        String decision = String.valueOf(result.get("decision"));
        switch (decision) {
            case "allow" -> accounts.create(email, null, false); // null: full trial
            case "limit" -> accounts.create(email, 3, false);
            case "shadow" -> { } // same response, nothing created
            case "challenge" -> {
                return ResponseEntity.status(403).body(Map.of("error", "confirm-email"));
            }
            default -> {
                return ResponseEntity.status(403).body(Map.of("error", "rejected"));
            }
        }
        return ResponseEntity.ok(Map.of("ok", true));
    }

    private Map<String, Object> nolgardVerify(String token, String action, String remoteIp, Map<String, Object> account) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("secret", Objects.requireNonNull(System.getenv("NOLGARD_SECRET"), "NOLGARD_SECRET is not set"));
        payload.put("token", token);
        payload.put("remoteIp", remoteIp);
        payload.put("action", action);
        if (account != null) {
            payload.put("account", account);
        }
        try {
            HttpRequest verify = HttpRequest.newBuilder(VERIFY_URL)
                    .timeout(Duration.ofSeconds(3))
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(payload)))
                    .build();
            HttpResponse<String> response = HTTP.send(verify, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() >= 500) {
                return UNAVAILABLE;
            }
            return json.readValue(response.body(), new TypeReference<Map<String, Object>>() {});
        } catch (IOException e) { // timeout, network error or bad JSON
            return UNAVAILABLE;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return UNAVAILABLE;
        }
    }
}
```

Supply your own `AccountService` bean with `create(email, trialDays, confirmEmailFirst)`; `trialDays` `null` means the full trial, and `confirmEmailFirst` holds the trial until the user clicks your confirmation link. Without a token (ad blocker, Brave Shields, no JavaScript) the user goes through email confirmation instead of a rejection. The code targets Spring Boot 3 with Jackson 2. If you use Spring Security, keep CSRF protection on and send its token with the form. Behind a proxy, `getRemoteAddr()` returns the proxy address; set `server.forward-headers-strategy` only when your proxy is trusted. Do not trust unchecked forwarding headers.
