Custom Implementation Example
本指南演示了如何构建一个完整的自定义安全实现,使用基于会话的身份验证。通过从头开始实现四个核心接口,您将学习它们如何协同工作。
大多数应用程序应使用 Spring Security
Spring Security 集成 会自动配置此处所示的所有内容。仅在您有特定需求或不使用 Spring Boot 时才构建自定义安全功能。
您将构建的内容
一个包含四个类的工作安全系统:
- SecurityConfiguration - 定义安全行为和重定向位置
- SecurityContext - 使用 HTTP 会话跟踪谁已登录
- SecurityManager - 协调安全检查并提供登录/注销功能
- SecurityRegistrar - 在应用程序启动时将所有内容连接在一起
此示例使用基于会话的存储,但您可以使用数据库查询、LDAP 或任何其他身份验证后端实现相同的接口。
各部分如何协同工作
流程:
SecurityRegistrar在启动时运行,创建管理器,注册评估器并附加观察者SecurityManager协调所有内容 - 它为评估器提供上下文和配置SecurityContext通过读取 HTTP 会话回答“谁已登录?”SecurityConfiguration为登录和拒绝访问页面回答“在哪里重定向?”Evaluators使用上下文和配置做出访问决策
第一步:定义安全配置
该配置告诉安全系 统如何行为以及在哪里重定向用户:
SecurityConfiguration.java
package com.securityplain.security;
import com.webforj.router.history.Location;
import com.webforj.router.security.RouteSecurityConfiguration;
import java.util.Optional;
/**
* 应用程序的安全配置。
*
* <p>
* 定义在需要身份验证或访问被拒绝时重定向用户的位置。
* </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- 安全功能处于活动状态isSecureByDefault() = false- 路由为公共,除非注释(使用true在默认情况下要求所有路由进行身份验证)/login- 未经过身份验证的用户去的地方/access-denied- 已经过身份验证但没有权限的用户去的地方
第二步:实现安全上下文
上下文跟踪谁已登录。此实现使用 HTTP 会话存储用户信息:
工作原理:
isAuthenticated()检查会话中是否存在用户主体getPrincipal()从会话存储中检索用户名hasRole()检查用户的角色集是否包含指定角色getAttribute()/setAttribute()管理自定义安全属性Environment.getSessionAccessor()提供线程安全的会话访问
第三步:创建安全管理器
管理器协调安全决策。它扩展了 AbstractRouteSecurityManager,后者处理评估器链和拒绝访问:
工作原理:
- 扩展
AbstractRouteSecurityManager以继承评估器链逻辑 - 提供
getConfiguration()和getSecurityContext()的实现 - 添加
login()以验证用户并存储会话中的凭据 - 添加
logout()以清除会话并重定向到登录页面 - 使用
SessionObjectTable进行简单的会话存储 - 将自身存储在
ObjectTable中以便于全局访问
第四步:在启动时连接所有内容
注册程序在应用程序启动时连接所有部分:
SecurityRegistrar.java
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;
/**
* 在应用程序启动期间注册路由安全组件。
*
* <p>
* 设置安全管理器和评估器与路由器一起工作。
* </p>
*/
@AppListenerPriority(1)
public class SecurityRegistrar implements AppLifecycleListener {
/**
* {@inheritDoc}
*/
@Override
public void onWillRun(App app) {
// 创建安全管理器
SecurityManager securityManager = new SecurityManager();
securityManager.saveCurrent(securityManager);
// 按优先级注册内置评估器
securityManager.registerEvaluator(new DenyAllEvaluator(), 0);
securityManager.registerEvaluator(new AnonymousAccessEvaluator(), 1);
securityManager.registerEvaluator(new PermitAllEvaluator(), 2);
securityManager.registerEvaluator(new RolesAllowedEvaluator(), 3);
// 创建安全观察者并附加到路由器
RouteSecurityObserver securityObserver = new RouteSecurityObserver(securityManager);
Router router = Router.getCurrent();
if (router != null) {
router.getRenderer().addObserver(securityObserver);
}
}
}
注册监听器:
创建 src/main/resources/META-INF/services/com.webforj.AppLifecycleListener,内容为:
com.securityplain.security.SecurityRegistrar
这会注册您的 AppLifecycleListener,以便它在应用程序启动时运行。
工作原理:
- 运行较早(
@AppListenerPriority(1))以在路由加载之前设置安全性 - 创建安全管理器并全局存储
- 按优先级顺序注册内置评估器(数字越小,优先级越高,先运行)
- 创建拦截导航的观察者
- 将观察者附加到路由器,以便安全检查自动发生
在此运行后,所有导航的安全性均处于活动状态。
使用您的实现
创建登录视图
以下视图使用 Login 组件。
LoginView.java
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("登录")
@AnonymousAccess
public class LoginView extends Composite<Login> {
private final 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());
}
}