在 Vaadin 14 Web 应用程序中发生视图 class 实例化之前,如何拦截检查授权的请求?

How can I intercept a request to check authorization before the view class instantiation occurs in a Vaadin 14 web app?

我正在尝试将 Apache Shiro's Annotation-based Authorization (authz) 集成到 Vaadin 14 网络应用程序中。我研究了一些关于这个主题的公开解决方案:

他们似乎都使用相同的解决方案,设置一个 VaadinServiceInitListener to add a BeforeEnterEvent 必须实现所有 authz 逻辑的侦听器。

我认为这种解决方案会出现一个问题:

Since Vaadin views are built from the class constructor, the target view class as already been instantiated when reaching the BeforeEnterEvent authz validation, which means that the view is processed whether or not the requester has authorization to access it.

如何在实例化 View 类 之前拦截请求,以便我可以使用 Shiro 的注释进行适当的 authz 检查?

在导航目标 class 实例化之前,目前没有任何好的拦截导航请求的方法。

作为一般解决方法,您应该确保您没有在 class 的构造函数中执行任何可能具有安全隐患的操作,而是使用 onAttach 等方法,因为它们将被调用仅在 BeforeEnterEvent 被调度后且仅当没有侦听器阻止导航时。

最近通过 issue #4595 在核心框架中解决了这个问题。尚未决定在哪个版本的 Vaadin 中引入更改,因为它们在某些情况下会影响向后兼容性。

由于 Vaadin 不提供 "any good way of intercepting navigation requests before the navigation target class is instantiated",我走了漫长而曲折的道路,想出了一个基于自定义 Interceptor 解决方案。这允许我在视图 class 实例化发生之前短路请求,结果如下:

  • 优点:

    • 安全性 - 未经授权的用户不会执行代码;

    • 性能 - 由于我之前执行了 authz 验证,因此 运行 不必要代码的所有开销都消失了。

  • 缺点:
    • 需要使用额外的注解。如果有一个解决方案可以与现有的 Shiro 注释无缝集成,那就太好了,欢迎任何改进。

解决方案

添加新的@ShiroSecured注解与拦截器绑定:

package misc.app.security;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.interceptor.InterceptorBinding;


@Inherited
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
@InterceptorBinding
public @interface ShiroSecured { }

添加一个ShiroSecuredInterceptor并将其绑定到之前定义的自定义注解。确保有一个用 @AroundConstruct 注释的方法,这样对 classes 的构造函数的调用被 @ShiroSecured 注释被拦截:

package misc.app.security;

import javax.interceptor.AroundConstruct;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;


/**
 * An interceptor for declarative security checks using the annotations from the 
 * {@code org.apache.shiro.authz.annotation} package.
 *
 */
@ShiroSecured @Interceptor
public class ShiroSecuredInterceptor extends AnnotationsAuthorizingConstructorInterceptor {

    @AroundConstruct
    public Object checkAuthorization(final InvocationContext ic) throws Exception {
        assertAuthorized(new InvocationContextToConstructorInvocationConverter(ic));

        return ic.proceed();
    }

    private static class InvocationContextToConstructorInvocationConverter implements ConstructorInvocation {

        private final InvocationContext context;


        public InvocationContextToConstructorInvocationConverter(InvocationContext ic) {
            context = ic;
        }

        @Override
        public Object proceed() throws Throwable {
            return context.proceed();
        }

        @Override
        public Class getClazz() {
            return context.getConstructor().getDeclaringClass();
        }
    }   
}

beans.xml中注册拦截器:

<?xml version="1.0" encoding="UTF-8"?>
<beans bean-discovery-mode="all" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">

    <interceptors>
        <class>misc.app.security.ShiroSecuredInterceptor</class>
    </interceptors>
</beans>

这总结了解决方案的主要部分。以下屏幕截图列出了我为使其工作而实施的所有 class 文件。基本上我不得不 复制 Shiro's logic related to MethodInvocation to make it work for ConstructorInvocation. The two Error classes are implementation of Vaadin's Router Exception Handling for Shiro's AuthorizationException and UnauthenticatedException 的一部分,当 authz 验证失败时抛出。