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. Itsactionisallow,challengeorblock. 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.