Static rules ("Block after 3 failed attempts") are necessary, but they are predictable. Sophisticated attackers know these rules. They rotate IPs. They sleep between attempts. They use valid credentials purchased from the dark web (Credential Stuffing).
To catch these, we need Behavioral Analysis. We need a system that has "intuition."
In this guide, we will build a "Risk Engine" using Spring Security Events and Spring AI. It sounds like Science Fiction, but with the tools we have today, it's surprisingly accessible.
The Concept
- Intercept: Listen for every login (Success or Failure).
- Enrich: Add context (GeoIP, Device Fingerprint, Time of Day).
- Analyze: Ask an AI model: "Given this user usually logs in from London on a Mac, is this login from a Linux server in Panama suspicious?"
- React: If Risk > 80, revoke session or trigger MFA.

Step 1: Listening to Events
Spring Security publishes events automatically. We just need to catch them.
@Component
public class AuthEventListener {
private final RiskAnalysisService riskService;
public AuthEventListener(RiskAnalysisService riskService) {
this.riskService = riskService;
}
@EventListener
@Async // Crucial: Don't block the login thread!
public void onSuccess(AuthenticationSuccessEvent event) {
Authentication auth = event.getAuthentication();
WebAuthenticationDetails details = (WebAuthenticationDetails) auth.getDetails();
LoginContext ctx = new LoginContext(
auth.getName(),
details.getRemoteAddress(),
LocalDateTime.now()
);
riskService.analyze(ctx);
}
@EventListener
@Async
public void onFailure(AbstractAuthenticationFailureEvent event) {
// Track brute force patterns
}
}Step 2: The Spring AI Risk Engine
This is where the magic happens. We construct a prompt for the LLM.
@Service
public class RiskAnalysisService {
private final ChatClient chatClient;
private final UserHistoryRepository historyRepo;
public RiskAnalysisService(ChatClient.Builder builder, UserHistoryRepository repo) {
this.chatClient = builder.build();
this.historyRepo = repo;
}
public void analyze(LoginContext ctx) {
// 1. Fetch historical baseline
List<LoginEvent> distinctHistory = historyRepo.findRecentDistinctLogins(ctx.username());
// 2. Construct Prompt
String prompt = """
You are a security analyst.
User '%s' is attempting to login from IP %s.
Their recent history includes: %s.
Analyze the probability of account takeover.
Return ONLY a JSON object: {"riskScore": 0-100, "reason": "..."}
""".formatted(ctx.username(), ctx.ip(), distinctHistory);
// 3. Call AI (Ollama/OpenAI)
String response = chatClient.prompt(prompt).call().content();
handleRiskDecision(response, ctx.username());
}
}Step 3: Taking Action
If the AI returns a high risk score, we can't block the login (it already happened), but we can kill the session immediately.
private void handleRiskDecision(String jsonResponse, String username) {
RiskAssessment assessment = parse(jsonResponse);
if (assessment.score > 80) {
log.warn("HIGH RISK LOGIN DETECTED: {}", assessment.reason);
// Revoke all sessions for this user
sessionRegistry.getAllSessions(username, false)
.forEach(SessionInformation::expireNow);
// Trigger Email Alert
notificationService.sendSecurityAlert(username, assessment.reason);
}
}Vector Databases for Scale
Passing raw text history to an LLM context window is expensive. For production, you would use Vector Search.
- Store login metadata embeddings in pgvector or Weaviate.
- When a user logs in, embed the current context.
- Query distance to previous logins.
- If distance is large (Vector Anomaly), flag it.
Spring AI supports VectorStore interfaces exactly for this purpose.
Conclusion
By merging the event-driven architecture of Spring Security with the probabilistic reasoning of Spring AI, we move from "Rules" to "Intuition".
This allows us to catch novel attacks that static IF-statements would miss. And honestly? It's just really cool to build.
