3.5.18. Web 登录

本节介绍 Web 客户端身份验证的工作原理以及如何在项目中进行扩展。有关中间层身份验证的信息,请参阅登录

Web 客户端 block 的登录过程的实现机制如下:

  • ConnectionImpl 实现了 Connection

  • LoginProvider 实现。

  • HttpRequestFilter 实现。

WebLoginStructure
Figure 28. Web 客户端的登录机制

Web 登录子系统的主要接口是 Connection,它包含以下关键方法:

  • login() - 验证用户、启动会话并更改连接状态。

  • logout() - 退出系统。

  • substituteUser() - 用另一个用户替换当前会话中的用户。此方法会创建一个新的 UserSession 实例,但会话 ID 不变。

  • getSession() - 获取当前用户会话。

成功登录后,ConnectionUserSession 对象存储到 VaadinSession 的属性中并设置 SecurityContextConnection 对象被绑定到 VaadinSession,因此无法从非 UI 线程使用它,如果在非 UI 线程调用 login/logout ,则会抛出 IllegalConcurrentAccessException

通常,登录是通过 AppLoginWindow 界面执行的,该界面支持使用用户名/密码和“记住我”凭据登录。

Connection 的默认实现是 ConnectionImpl,它将登录委托给 LoginProvider 实例链。LoginProvider 是一个可以处理特定 Credentials 实现的登录模块,它还有一个特殊的 supports() 方法,允许调用者查询它是否支持给定的 Credentials 类型。

WebLoginProcedure
Figure 29. 标准用户登录过程

标准用户登录过程:

  • 用户输入用户名和密码。

  • Web 客户端 block 创建一个 LoginPasswordCredentials 对象,将用户名和密码传递给其构造函数,并使用此凭据调用 Connection.login() 方法。

  • Connection 查找对象 LoginProvider 对象链。 这种情况下使用的是 LoginPasswordLoginProvider ,它支持 LoginPasswordCredentials 凭据。LoginPasswordLoginProvider 使用 PasswordEncryption bean 的 getPlainHash() 方法散列密码,并调用 AuthenticationService.login(Credentials)。 根据 cuba.checkPasswordOnClient属性设置,它要使用户名和密码调用 AuthenticationService.login(Credentials) 方法;或者通过用户名加载 User 实体、根据加载的密码哈希验证密码,验证通过后使用 TrustedClientCredentialscuba.trustedClientPassword作为可信客户端登录。

  • 如果验证成功,则创建的具有活动UserSessionAuthenticationDetails 实例将被回传给 Connection

  • Connection 创建一个 ClientUserSession 包装器并将其设置到 VaadinSession 的属性中。

  • Connection 创建一个 SecurityContext 实例并将其设置为 AppContext

  • Connection 触发 StateChangeEvent,此事件会触发 UI 更新和 AppMainWindow 初始化。

所有 LoginProvider 实现必须:

  • 使用 Credentials 对象验证用户。

  • 使用 AuthenticationService 启动新用户会话或返回另一个活动会话(例如,匿名的)。

  • 返回身份验证详细信息,如果无法使用此 Credentials 对象登录用户,则返回空,例如,如果登录提供程序已被禁用或未正确配置。

  • 如果出现错误的 Credentials,则抛出 LoginException 或将 LoginException 从中间件传递给调用者。

HttpRequestFilter - bean 的标记接口,这种 bean 将作为 HTTP 过滤器自动被添加到应用程序过滤器链: https://docs.oracle.com/javaee/6/api/javax/servlet/Filter.html 。可以使用它来实现其它形式的身份验证、对 HTTP 请求和响应进行预处理或后处理。

要添加额外的 Filter , 可以创建 Spring Framework 组件并实现 HttpRequestFilter 接口:

@Component
public class CustomHttpFilter implements HttpRequestFilter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {
        // delegate to the next filter/servlet
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

请注意,最简单的实现必须将执行委托给 FilterChain,否则应用程序将无法工作。默认情况下,作为 HttpRequestFilter bean 被添加的过滤器将不会收到对 VAADIN 目录和 cuba.web.cubaHttpFilterBypassUrls app 属性中指定的其它路径的请求。

内置登录提供程序

平台包含以下 LoginProvider 接口的实现:

  • AnonymousLoginProvider - 为不需登录的用户提供匿名登录。

  • LoginPasswordLoginProvider -将登录委托给使用 LoginPasswordCredentialsAuthenticationService

  • RememberMeLoginProvider- 将登录委托给使用 RememberMeCredentialsAuthenticationService

  • LdapLoginProvider - 授受 LoginPasswordCredentials 参数,使用 LDAP 执行身份验证并将登录委托给使用 TrustedClientCredentialsAuthenticationService 服务。

  • ExternalUserLoginProvider - 授受 ExternalUserCredentials 参数,将登录委托给使用 TrustedClientCredentialsAuthenticationService 服务。可使用提供的用户名执行登录。

所有实现都使用 AuthenticationService.login() 创建一个活动的用户会话。

可以使用 Spring Framework 的机制覆盖它们中的任何一个。

事件

Connection 的标准实现 - ConnectionImpl 在登录过程中触发以下应用程序事件

  • BeforeLoginEvent / AfterLoginEvent

  • LoginFailureEvent

  • UserConnectedEvent / UserDisconnectedEvent

  • UserSessionStartedEvent / UserSessionFinishedEvent

  • UserSessionSubstitutedEvent

BeforeLoginEventLoginFailureEvent 的事件处理程序可能抛出 LoginException 来取消登录过程或覆盖初始登录失败异常。

例如,可以使用 BeforeLoginEvent 实现只允许登录名中包含有公司域名的用户登录 Web 客户端。

@Component
public class BeforeLoginEventListener {
    @Order(10)
    @EventListener
    protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException {
        if (event.getCredentials() instanceof LoginPasswordCredentials) {
            LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) event.getCredentials();

            if (loginPassword.getLogin() != null
                    && !loginPassword.getLogin().contains("@company")) {
                throw new LoginException(
                        "Only users from @company are allowed to login");
            }
        }
    }
}

此外,标准应用程序类 - DefaultApp 会触发以下事件:

  • AppInitializedEvent - 在 App 初始化后触发,每个 HTTP 会话执行一次。

  • AppStartedEvent - 在以匿名用户身份登录进行第一次请求处理时触发。事件处理器可以使用绑定到 AppConnection 对象来完成用户登录。

  • AppLoggedInEvent - 用户登录成功时的 App UI 初始化后触发。

  • AppLoggedOutEvent - 用户注销时的 App UI 初始化后触发。

  • SessionHeartbeatEvent - 收到来自客户端 Web 浏览器的心跳请求时触发。

AppStartedEvent 可用于使用第三方认证系统实现 SSO 登录,例如 Jasig CAS。通常,它与自定义 HttpRequestFilter bean 一起使用,该 bean 应收集并提供其它身份验证数据。

我们假设:如果用户有一个特殊的 cookie 值 - PROMO_USER,应用程序将自动登录。

@Order(10)
@Component
public class AppStartedEventListener implements ApplicationListener<AppStartedEvent> {

    private static final String PROMO_USER_COOKIE = "PROMO_USER";

    @Inject
    private Logger log;

    @Override
    public void onApplicationEvent(AppStartedEvent event) {
        String promoUserLogin = event.getApp().getCookieValue(PROMO_USER_COOKIE);
        if (promoUserLogin != null) {
            Connection connection = event.getApp().getConnection();
            if (!connection.isAuthenticated()) {
                try {
                    connection.login(new ExternalUserCredentials(promoUserLogin));
                } catch (LoginException e) {
                    log.warn("Unable to login promo user {}: {}", promoUserLogin, e.getMessage());
                } finally {
                    event.getApp().removeCookie(PROMO_USER_COOKIE);
                }
            }
        }
    }
}

因此,如果用户拥有“PROMO_USER”cookie 并打开应用程序,它们将自动以 promoUserLogin 身份登录。

如果要在登录和 UI 初始化后执行其它操作,可以使用 AppLoggedInEvent。 需要注意的是,在事件处理程序中必须检查用户是否进行了身份验证,因为所有事件也会对 anonymous 用户触发。

扩展点

可以使用以下类型的扩展点扩展登录机制:

  • Connection - 替换现有的 ConnectionImpl

  • HttpRequestFilter - 实现额外的 HttpRequestFilter

  • LoginProvider 实现 - 实现额外的或替换现有的 LoginProvider

  • 事件 - 为一个可用的事件实现事件处理器。

可以使用 Spring Framework 机制替换现有 bean,例如通过在 web 模块的 Spring XML 配置中注册新 bean。

<bean id="cuba_LoginPasswordLoginProvider"
      class="com.company.demo.web.CustomLoginProvider"/>
过时/弃用

可以通过设置以下应用程序属性来启用 CubaAuthProvider 接口的自定义实现:

cuba.web.externalAuthentication = true
cuba.web.externalAuthenticationProviderClass = com.company.sample.web.MyAuthProvider

现已弃用以下组件:

  • CubaAuthProvider 及其实现在兼容模式下可用。使用事件 、LoginProviderHttpRequestFilter 代替。

  • LdapAuthProvider 已替换为 LdapLoginProvider,可按这里的描述启用:LDAP 集成

  • IdpAuthProvider 已替换为 IdpLoginProvider,可按 IDP 插件 wiki 的描述启用。

不要使用这些组件。它们会在平台的下一个主版本中被删除。

请使用WEB 登录扩展点代替。