Sprisk Engine is a lightweight and extensible risk scoring engine for Spring Boot, designed to detect suspicious login behavior such as brute force, credential stuffing, and velocity anomalies.
This guide explains how the Sprisk Engine modules work together, how to integrate the Spring Boot starter into your applications, and where you can customise behaviour. It is intended both for application teams that consume the starter and for engineers extending the engine itself.
-
Add the dependency
repositories { mavenCentral() } dependencies { implementation("io.github.sahinemirhan:sprisk-engine-starter:0.0.2") } -
Annotate the endpoint – decorate the HTTP or service method that should trigger a risk evaluation:
@GetMapping("/transfer") @RiskCheck(action = "TRANSFER", evaluateOnFailure = true) public ResponseEntity<?> transfer(...) { ... }The action attribute becomes part of the risk profile and helps with reporting and rule configuration.
-
Provide a user identifier – Sprisk requires a unique user identifier for every risk evaluation. Reference the identifier with the SpEL expression on @RiskCheck either at controller or service level:
// From a header @RiskCheck(userId = "#headers['X-User-Id']") // From a request parameter @GetMapping("/test") @RiskCheck(userId = "#id") or @RiskCheck(userId = "#request.getParameter('name')") public String getUser(@RequestParam String id){ return id; }; // From a path variable @GetMapping("/test/{id}") @RiskCheck(userId = "#id") or @RiskCheck(userId = "#pathVariables['id']") public String getUser(@PathVariable String id) { return id; } // From a request attribute set by a filter @RiskCheck(userId = "#request.getAttribute('sprisk.userId')") // From the Spring Security principal @RiskCheck(userId = "#request.userPrincipal?.name")
If you want to place the user id on the request yourself, register a simple filter:
-
Challenge / block behaviour – By default the starter throws exceptions when a challenge or block decision is reached. If you want JSON responses or other behaviour, register your own ChallengeHandler and/or BlockHandler. Whatever type your handler returns through ChallengeResolution.returning(...) must match the annotated method signature (for example ResponseEntity<?> in the demo app). If you want to return different types per endpoint, tailor the handler accordingly or keep the default exception-throwing strategy.
-
Redis (optional) – If your application exposes a StringRedisTemplate bean the starter automatically pings Redis. A successful ping enables the Redis-backed storage and logs [Sprisk] RedisStorage activated. If the ping fails you will see [Sprisk] Redis unavailable, falling back to InMemoryStorage and the in-memory store takes over transparently.
| Core | sprisk-engine-core | RuleEngine, RiskResult, DecisionProfile, low-level rule interfaces |
| Spring Starter | sprisk-engine-starter | Auto-configuration, @RiskCheck, RiskAspect, hard-rule evaluation, handlers |
| Example App | sprisk-engine-example-app | Demonstrates handlers, listeners, composite resolver, and integration patterns |
To enable Redis support simply expose StringRedisTemplate in your application context (see section 10).
- RiskAspect intercepts the @RiskCheck method and builds a RiskInvocation.
- RuleEngine executes every registered RiskRule.
- The combined score and triggered rules are stored in a RiskResult.
- HardRuleEvaluator checks both the defaults and the YAML-defined hard rules.
- DecisionProfile compares the score to the configured challenge and block thresholds.
- If the decision is CHALLENGE or BLOCK, the relevant handler is invoked and returns a ChallengeResolution.
- ChallengeOutcomeListener beans fire, letting you push metrics or logs downstream.
- The current HttpServletRequest receives spriskRuleFlags and spriskRuleFlagsString attributes for debugging or telemetry.
Priority order:
(1) Rule class defaults → (2) application.yaml → (3) class-level @RiskCheck → (4) method-level @RiskCheck → (5) programmatic overrides.
- ChallengeResolution.proceed()/returning()/throwing() drive how execution continues.
- ChallengeOutcome carries status, TTL, persistence, and metadata.
- ChallengePolicyStrategy determines which policy applies to the current request.
- Default handlers throw exceptions; supply your own implementations for REST-friendly responses or custom flows.
- When you return a value using ChallengeResolution.returning(...), ensure the type matches what the intercepted method expects (string, DTO, ResponseEntity, etc.).
Hard rules are defined under sprisk.hard-rules. The engine loads every rule irrespective of its identifier, so you can freely add names like fraud-block or vip-allow. Each rule declares a match map that references other rule codes and an action to execute (BLOCK or CHALLENGE). Keep the keys aligned with RiskRule.code() outputs.
Example:
Rules are evaluated in YAML order; place more specific matches first.
| IP_VELOCITY | Tracks per-IP request rate | windowSeconds=60, maxPerWindow=50, riskScore=30 |
| USER_VELOCITY | Tracks request rate per user id | windowSeconds=60, maxPerWindow=20, riskScore=40 |
| BRUTE_FORCE | Counts failed attempts | enabled=false, windowSeconds=300, maxFail=5, riskScore=60 |
| CREDENTIAL_STUFFING | Detects many user ids from the same IP | enabled=false, windowSeconds=300, maxDistinctUserCount=20, riskScore=70 |
| NIGHT_TIME | Flags activity during night hours | enabled=true, startHour=2, endHour=6, riskScore=15 |
Rules are customisable via YAML:
The starter automatically gathers every RiskRule bean in the Spring context. To add your own heuristics, implement the interface and either annotate the class with @Component or expose it via a @Bean. Return a unique code() (used by hard rules and logs) and an evaluate score greater than zero when the rule should contribute risk.
If you prefer Java configuration, declare the rule inside a configuration class:
As soon as the bean exists, RuleEngine logs it during startup and evaluates it alongside the built-ins. You can reference the returned code() in YAML hard rules or overrides exactly as you do with the default rules.
When a StringRedisTemplate bean is present the starter issues a PING before enabling Redis storage:
- Successful ping → [Sprisk] RedisStorage activated (Redis connection successful)
- Failed ping → [Sprisk] Redis unavailable, falling back to InMemoryStorage
Redis configuration example:
For local development you can spin up Redis with docker run --rm -p 6379:6379 redis:7-alpine.
| IP velocity triggers incorrectly | Client IP not forwarded | Add ForwardedHeaderFilter or override @RiskCheck(ip = ...) |
| User id resolves to null | Resolver chain cannot locate an id | Ensure headers/session/JWT provide a user id |
| Default hard rule blocks a user | Same user accesses from multiple IPs | Tweak or disable sprisk.hard-rules.distributed-user-attack |
| Redis keys keep growing | TTL/window values too long | Revisit sprisk.policy.* and per-rule window settings |
Use the example app’s RiskDebugLoggingFilter to inspect request attributes during development.
The published artefact coordinates are io.github.sahinemirhan:sprisk-engine-starter. Releases are tagged in Git with matching versions, and each release includes sources and javadoc jars. If you want to depend on an unreleased build, use ./gradlew publishToMavenLocal and point your consuming project at mavenLocal() during development.
- Fork the repository and create feature branches from main.
- Update documentation (docs/) whenever you add features or behaviour flags.
- Add unit or integration tests for changes that affect challenge/block logic or rule outcomes.
- Run ./gradlew build before opening a pull request and attach the relevant output.
- Follow the existing code style and avoid introducing unnecessary dependencies.
The project welcomes issues and discussions on GitHub. Bug reports with reproduction steps and proposed improvements are especially helpful.
For questions, open an issue in the repository or reach out to the maintainers. Happy building!
.png)


