Custom Implementation Example
This guide walks through building a complete custom security implementation using session-based authentication. You'll learn how the four core interfaces work together by implementing them from scratch.
The Spring Security integration auto-configures everything shown here. Only build custom security if you have specific requirements or aren't using Spring Boot.
What you'll build
A working security system with four classes:
- SecurityConfiguration - Defines security behavior and redirect locations
- SecurityContext - Tracks who is logged in using HTTP sessions
- SecurityManager - Coordinates security checks and provides login/logout
- SecurityRegistrar - Wires everything together at app startup
This example uses session-based storage, but you could implement the same interfaces using database queries, LDAP, or any other authentication backend.
How the pieces work together
Flow:
SecurityRegistrarruns at startup, creates the manager, registers evaluators, and attaches the observerSecurityManagercoordinates everything - it provides the context and configuration to evaluatorsSecurityContextanswers "Who is logged in?" by reading from HTTP sessionsSecurityConfigurationanswers "Where to redirect?" for login and access denied pagesEvaluatorsmake access decisions using the context and configuration
Step 1: Define security configuration
The configuration tells the security system how to behave and where to redirect users:
package com.securityplain.security;
import com.webforj.router.history.Location;
import com.webforj.router.security.RouteSecurityConfiguration;
import java.util.Optional;
/**
* Security configuration for the application.
*
* <p>
* Defines where to redirect users when authentication is required or access is denied.
* </p>
*/
public class SecurityConfiguration implements RouteSecurityConfiguration {
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean isSecureByDefault() {
return false;
}
@Override
public Optional<Location> getAuthenticationLocation() {
return Optional.of(new Location("/login"));
}
@Override
public Optional<Location> getDenyLocation() {
return Optional.of(new Location("/access-denied"));
}
}
isEnabled() = true- Security is activeisSecureByDefault() = false- Routes are public unless annotated (usetrueto require authentication on all routes by default)/login- Where unauthenticated users go/access-denied- Where authenticated users without permissions go
Step 2: Implement security context
The context tracks who is logged in. This implementation uses HTTP sessions to store user information:
How it works:
isAuthenticated()checks if a user principal exists in the sessiongetPrincipal()retrieves the username from session storagehasRole()checks if the user's role set contains the specified rolegetAttribute()/setAttribute()manage custom security attributesEnvironment.getSessionAccessor()provides thread-safe session access
Step 3: Create security manager
The manager coordinates security decisions. It extends AbstractRouteSecurityManager, which handles evaluator chains and access denial:
How it works:
- Extends
AbstractRouteSecurityManagerto inherit evaluator chain logic - Provides
getConfiguration()andgetSecurityContext()implementations - Adds
login()to authenticate users and store credentials in session - Adds
logout()to clear the session and redirect to the login page - Uses
SessionObjectTablefor simple session storage - Stores itself in
ObjectTablefor app-wide access
Step 4: Wire everything at startup
The registrar connects all the pieces when the app starts:
package com.securityplain.security;
import com.webforj.App;
import com.webforj.AppLifecycleListener;
import com.webforj.annotation.AppListenerPriority;
import com.webforj.router.Router;
import com.webforj.router.security.RouteSecurityObserver;
import com.webforj.router.security.evaluator.AnonymousAccessEvaluator;
import com.webforj.router.security.evaluator.DenyAllEvaluator;
import com.webforj.router.security.evaluator.PermitAllEvaluator;
import com.webforj.router.security.evaluator.RolesAllowedEvaluator;
/**
* Registers route security components during application startup.
*
* <p>
* Sets up security manager and evaluators with the router.
* </p>
*/
@AppListenerPriority(1)
public class SecurityRegistrar implements AppLifecycleListener {
/**
* {@inheritDoc}
*/
@Override
public void onWillRun(App app) {
// Create security manager
SecurityManager securityManager = new SecurityManager();
securityManager.saveCurrent(securityManager);
// Register built-in evaluators with priorities
securityManager.registerEvaluator(new DenyAllEvaluator(), 0);
securityManager.registerEvaluator(new AnonymousAccessEvaluator(), 1);
securityManager.registerEvaluator(new PermitAllEvaluator(), 2);
securityManager.registerEvaluator(new RolesAllowedEvaluator(), 3);
// Create security observer and attach to router
RouteSecurityObserver securityObserver = new RouteSecurityObserver(securityManager);
Router router = Router.getCurrent();
if (router != null) {
router.getRenderer().addObserver(securityObserver);
}
}
}
Register the listener:
Create src/main/resources/META-INF/services/com.webforj.AppLifecycleListener with:
com.securityplain.security.SecurityRegistrar
This registers your AppLifecycleListener so it runs at app startup.
How it works:
- Runs early (
@AppListenerPriority(1)) to set up security before routes load - Creates the security manager and stores it globally
- Registers built-in evaluators in priority order (lower numbers run first)
- Creates the observer that intercepts navigation
- Attaches the observer to the router so security checks happen automatically
After this runs, security is active for all navigation.
Using your implementation
Create a login view
The following view uses the Login component.
package com.securityplain.views;
import com.securityplain.security.SecurityManager;
import com.webforj.component.Composite;
import com.webforj.component.login.Login;
import com.webforj.router.Router;
import com.webforj.router.annotation.FrameTitle;
import com.webforj.router.annotation.Route;
import com.webforj.router.history.Location;
import com.webforj.router.security.annotation.AnonymousAccess;
@Route("/login")
@FrameTitle("Login")
@AnonymousAccess
public class LoginView extends Composite<Login> {
private Login self = getBoundComponent();
public LoginView() {
self.onSubmit(e -> {
var result = SecurityManager.getCurrent().login(
e.getUsername(), e.getPassword()
);
if (result.isGranted()) {
Router.getCurrent().navigate(new Location("/"));
} else {
self.setError(true);
self.setEnabled(true);
}
});
self.whenAttached().thenAccept(c -> self.open());
}
}