← All docs

Spring Boot

A starter that registers LayerSift's Java client, guard and Servlet filter in a Spring Boot application, from antibot.* properties. Set the secret key and your sign-ins, redemptions and messages are assessed before your controllers and before Spring Security answers them.

It is the Java SDK with the Spring Boot wiring around it, and the same rules apply: the guard decides what the edge decides, and the filter is the Jakarta Servlet adapter of it.

Install

There is no published artifact yet, so the starter is built from this repository, with the SDK's sources compiled into it. From a checkout:

cd integrations/spring-boot
mvn -B install

Then depend on it in your application:

<dependency>
    <groupId>dev.antibot</groupId>
    <artifactId>antibot-spring-boot-starter</artifactId>
    <version>0.1.0-SNAPSHOT</version>
</dependency>

It needs a servlet web application: spring-boot-starter-web (Tomcat, which the tests run in), or any Servlet 6 container your application already uses. A reactive (WebFlux) application gets nothing registered: the filter is a Servlet filter. The starter does not bring a web stack itself. Java 17 or newer.

Configure

Set antibot.secret-key and the starter is on. Use a key that is allowed to assess and nothing else.

antibot:
  secret-key: ${ANTIBOT_ASSESS_KEY}
  endpoint: https://api.antibot.dev         # the default
  # session-keys: key-a,key-b               # comma-separated; verify receipts here without a call
  protect:                                  # path prefixes; unset: every path
    - /shop
    - /api
  logins:                                   # sign-ins: the field is the account
    - path: /login
      field: username
  codes:                                    # redemptions: the field is the code
    - path: /gift-card/redeem
      field: card
  contents:                                 # messages: the field is what was written
    - path: /reviews
      field: review.body                    # a dotted path into JSON
  block-signals: bot                        # signals that condemn a receipt; default bot
  max-score: 70
  cookie: _abid
  token-header: antibot-token               # empty turns it off
  # bound-paths: /api/orders                # only a receipt bound to the page's key counts there
  # flows:                                  # the steps of your flows, in order
  #   checkout: POST /cart/checkout,/checkout/delivery,/checkout/payment@3000
  origin: https://shop.example              # when TLS ends upstream
  missing: allow                            # or challenge: see below
  step-up: off                              # or hold, hold-all: needs the challenge
  agent-on-pages: false
  monitor: false                            # decide, act on nothing: see Monitor mode
  challenge:                                # enabled by public-key
    public-key: ${ANTIBOT_PUBLIC_KEY}
    agent-url: /_ab/agent
    endpoint: https://api.antibot.dev       # defaults to antibot.endpoint
Property Guard option Notes
secret-key the client's key Setting it turns the starter on
endpoint the client's origin
timeout the client's timeout and timeout Default 2s: how long a request waits for its assessment before it goes on unassessed (and onError hears of it)
session-keys sessionKeys Without them every request is assessed
protect protect Prefixes, written as the application routes them or whole: under server.servlet.context-path=/shop, /login and /shop/login are both the request for /shop/login (the assessed URL keeps the context path), and so under spring.mvc.servlet.path=/shop. Configured forms are always covered
logins, codes, contents logins, codes, contents path and field, as above
block-signals blockSignals Unset: the guard's bot. An empty list: none
max-score maxScore A receipt at or above it is refused
cookie, token-header cookie, tokenHeader
flows flows The steps of each flow, in order, as [METHOD ]path[@minMs] (steps taken faster than a person)
graphql, graphql-persisted graphql, graphqlPersisted Your GraphQL endpoints (/graphql), and your trusted documents, id to text (GraphQL: one request that is many)
session-cookie sessionCookie Your session cookie by name (JSESSIONID, or SESSION under Spring Session): sent as a digest, never the cookie, so a signed-in session is bound to the device and browser that first used it (a signed-in session, bound to its device)
bound-paths boundPaths Prefixes only your pages call: there only a receipt bound to the page's key counts, with a fresh signature by it (API calls only your pages can make). Needs session-keys
origin origin
missing missing challenge runs the interstitial for a page without a passing receipt
step-up stepUp hold holds a receipt the verdict asked to step up to a press-and-hold; hold-all refuses its API calls too. Needs challenge
agent-on-pages agentOnPages Set it when your pages load the agent
monitor monitor true turns on monitor mode: nothing is refused or challenged. See below
challenge.* challenge Needs agent-url too; the app does not start without it

Without antibot.secret-key nothing is registered and one INFO line says so — a WARN when it is set and blank (${ANTIBOT_ASSESS_KEY:} with the variable unset), which meant to protect and does not. Requests are not assessed.

Ordering against Spring Security

The filter is registered with FilterRegistrationBean at Ordered.HIGHEST_PRECEDENCE + 10. That puts it:

  • after Spring's encoding and forwarded-header filters, so the address and the parameters are the ones your application reads;
  • ahead of Spring Security's filter chain (order -100), which answers a form login itself. A sign-in is assessed before Security decides it.

Don't declare AntibotFilter as a @Bean of its own. Boot then orders it after Spring Security, and a form login is never assessed.

To replace the registration, declare your own bean named antibotFilter. To replace the guard, client or client-address hook, declare a bean of that type; every bean is @ConditionalOnMissingBean.

Forwarded headers and the client's address

The guard assesses the address AntibotClientIp gives. By default that is request.getRemoteAddr(), which Spring resolves from the forwarded headers when server.forward-headers-strategy is set:

server:
  forward-headers-strategy: framework   # or native, for Tomcat's RemoteIpValve

framework believes X-Forwarded-For from whoever sends it, so use it only when nothing but your proxy can reach the server. Behind a CDN, the header a client cannot write is better. Set it through a bean:

@Bean
AntibotClientIp antibotClientIp() {
    return request -> request.header("cf-connecting-ip");
}

When the header is missing the guard falls back to the socket's address. With no address at all, the request is still assessed, as unknown.

Reading the verdict

Every protected request has a state, on the request attribute antibot:

@GetMapping("/login")
String login(HttpServletRequest request) {
    AntibotGuard.State antibot = AntibotFilter.stateOf(request);   // null on an unprotected path
    if (antibot != null && antibot.verdict() != null
            && "challenge".equals(antibot.verdict().get("action"))) {
        return "second-factor";            // a sign-in can ask for a second factor
    }
    return "password";
}
  • verdict() is the API's assessment, when one was made. Its action is allow, challenge or block. A blocked request never reaches your controller: it is answered with a 403 that carries a reference the visitor can quote (not in monitor mode, below).
  • A challenge on a configured form goes on to your handler, so the sign-in can show its second factor. Elsewhere a challenge is an interstitial for a browser, or a 403 the front end can act on.
  • receipt() holds the verified claims when the request passed on a receipt, when session keys are set.
  • wouldAct() is set only in monitor mode, and only when something would have been done. See below.

Monitor mode

Set antibot.monitor: true to watch before enforcing. The guard decides every request exactly as it otherwise would, and acts on none of it: a request that would be refused (a condemned receipt, a block from the API) or challenged (the interstitial, missing: challenge's first visit) goes on to your controller instead. Assessments are sent with monitor: true, so the API keeps them as monitored, not as done. Outcomes of sign-ins and redemptions are still reported.

It is for a site's first weeks: run it, read what would have been refused, and turn it off when the answers look right.

AntibotGuard.State antibot = AntibotFilter.stateOf(request);
if (antibot != null && antibot.wouldAct() != null) {
    log.info("antibot would {} this request: {}", antibot.wouldAct().action(), antibot.wouldAct().reason());
}

wouldAct() is an AntibotGuard.WouldAct with two fields: action (block or challenge) and reason, the reason the guard would have used (signal:bot, assess:block, absent, missing:challenge, ...). verdict() is still the assessment it was decided on. It is absent when nothing would have been done.

Reporting the outcome

A sign-in or a redemption is counted against its address, so report how it went. Either say it yourself:

AntibotGuard.State antibot = AntibotFilter.stateOf(request);
if (antibot != null) {
    antibot.outcome(passwordOk ? "success" : "failure");
}

or leave it to the response. Without a call, the outcome is read from the answer the handler gave and sent as inferred. A sign-in's:

  • 401 and 422 are failures;
  • a redirect back to the form is a failure, a redirect elsewhere a success;
  • a JSON 2xx is a success;
  • nothing, after a challenge.

A redemption's: 400, 404, 409, 410 and 422 are refusals, a JSON 2xx an acceptance, and anything else says nothing — a redirect too, since after post-redirect-get a refusal and an acceptance both redirect. A redemption that redirects reports its outcome with outcome(...).

Spring Security's form login answers a wrong password with a redirect to /login?error, so that counts as a failure with no code at all. A successful one, redirected elsewhere, counts as a success. Say the outcome yourself when your login does something else, since only a success you report is remembered as a device the account signs in from.

When the API cannot be reached

The request goes on, unassessed. The starter logs the failure at WARN, through commons-logging, with the guard's message. Outcome reports and receipt usage are sent behind the response, so they never delay it.

What the starter does not do yet

  • It is not published to Maven Central.

See also Server SDKs: Java for the guard's options in code, and Getting started for the enforcing options.