在不使用 Keycloak 的情况下在 Wildfly 中仅承载身份验证

Bearer only authentication in Wildfly without using Keycloak

我想在 Wildfly 中自己实现 Bearer-only 身份验证。本质上,我将执行以下步骤:

  1. 当我收到请求时,我会检查它是否有授权header。

  2. 我获取令牌并检查数据库(在本例中我将使用 Redis)以验证其有效性。

  3. 我从数据库中获取该用户的角色。

  4. 我希望能够在我的休息服务上使用 @RolesAllowed 注释。

我该怎么做?我需要如何修改 Wildfly 配置文件?我需要实现哪些接口?我如何将用户的角色传递给安全上下文,以便 Wildfly 为我进行 @RolesAllowed 检查?

如果回答,请考虑我是一位经验丰富的 Java 程序员,但对 Wildfly 是新手,因此您可以跳过有关编程逻辑的详细信息,但不会跳过有关 Wildfly 配置的详细信息。另外,在您的回答中,不要担心令牌最初是如何到达 Redis 的,或者客户是如何获得它的。

编辑

这就是我所做的,但还没有成功。我实现了一个实现了 ContainerRequestFilterAuthenticationFilter。 (我在下面只包括我已经实现的主要过滤器功能。请注意,有一些从数据库中获取角色的辅助功能未包括在内)。即使在函数结束时我使用用户配置文件(包含角色)设置请求上下文的安全上下文,我也无法在我的 JAX-RS 休息服务上使用 @RolesAllowed 注释.关于我应该做什么的任何指示?

注意:我没有修改任何 Wildfly 配置文件或 web.xml 文件。我知道每个请求都会调用过滤器,因为我能够在每个请求上记录来自它的消息。

/** 
 * (non-Javadoc)
 * @see javax.ws.rs.container.ContainerRequestFilter#filter(javax.ws.rs.container.ContainerRequestContext)
 */
@Override
public void filter(ContainerRequestContext requestContext) {

    //1. Read the JSON web token from the header
    String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
    if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
        requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
        return;
    }

    String token = authorizationHeader.substring("Bearer".length()).trim();

    try{
        //Note that if the token is not in the database,
        //an exception will be thrown and we abort.

        UserProfile userProfile = this.getUserProfile(token);

        if (null == userProfile){
            userProfile = this.decodeToken(token);
        }


        if (null == userProfile){
            throw new Exception();
        }


        String role = userProfile.getUserRole();
        if (null == role){
            role = this.getRoleFromMod(userProfile);
            if (null == role){
                role = RoleType.READ_ONLY;
            }
            userProfile.setUserRole(role);
            this.updateUserProfileForToken(token, userProfile);

        }

        userProfile.setUserRole(role);

        //5. Create a security context class that implements the crazy interface 
        //and set it here.
        requestContext.setSecurityContext(new ModSecurityContext(userProfile));

    }
    catch(Exception e){
        requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
    }
}

是的,我不确定它在 EE 环境中如何工作,甚至使资源 class 成为无状态 bean。 @RolesAllowed 注释旨在用于 ejbs。在这种情况下,主体是从 servlet 请求中检索的(我相信)。我要做的只是实现您自己的授权过滤器,该过滤器在安全上下文中查找注释并检查主体。

可以看到how Jersey implements it。除了 AnnotatedMethod class 之外,没有什么是 Jersey 特有的。为此,您可以使用 java.lang.reflect.Method (resourceInfo.getResourceMethod()) 进行一些反思。除此之外,您几乎可以按原样复制代码。完成后,只需在应用程序中注册 RolesAllowedDynamicFeature。或者只是用 @Provider 对其进行注释以进行扫描。

您还需要确保您的身份验证过滤器使用 @Priority(Priorities.AUTHENTICATION) 进行注释,以便在使用 @Priority(Priorities.AUTHORIZATION) 进行注释的授权过滤器之前调用它。


更新

这是我链接到的代码的重构,因此它不使用 Jersey 特定的 classes。 AnnotatedMethod 只是更改为 Method

@Provider
public class RolesAllowedFeature implements DynamicFeature {

    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext configuration) {
        Method resourceMethod = resourceInfo.getResourceMethod();
        
        if (resourceMethod.isAnnotationPresent(DenyAll.class)) {
            configuration.register(new RolesAllowedRequestFilter());
            return;
        }
        
        RolesAllowed ra = resourceMethod.getAnnotation(RolesAllowed.class);
        if (ra != null) {
            configuration.register(new RolesAllowedRequestFilter(ra.value()));
            return;
        }
        
        if (resourceMethod.isAnnotationPresent(PermitAll.class)) {
            return;
        }
        
        ra = resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
        if (ra != null) {
             configuration.register(new RolesAllowedRequestFilter(ra.value()));
        }
    }

    @Priority(Priorities.AUTHORIZATION) // authorization filter - should go after any authentication filters
    private static class RolesAllowedRequestFilter implements ContainerRequestFilter {

        private final boolean denyAll;
        private final String[] rolesAllowed;

        RolesAllowedRequestFilter() {
            this.denyAll = true;
            this.rolesAllowed = null;
        }

        RolesAllowedRequestFilter(final String[] rolesAllowed) {
            this.denyAll = false;
            this.rolesAllowed = (rolesAllowed != null) ? rolesAllowed : new String[]{};
        }

        @Override
        public void filter(final ContainerRequestContext requestContext) throws IOException {
            if (!denyAll) {
                if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
                    throw new ForbiddenException("Not Authorized");
                }

                for (final String role : rolesAllowed) {
                    if (requestContext.getSecurityContext().isUserInRole(role)) {
                        return;
                    }
                }
            }

            throw new ForbiddenException("Not Authorized");
        }

        private static boolean isAuthenticated(final ContainerRequestContext requestContext) {
            return requestContext.getSecurityContext().getUserPrincipal() != null;
        }
    }
}

首先让我解释一下 DynamicFeature 的工作原理。为此,让我们首先将讨论的上下文更改为您当前实施的 AuthenticationFilter.

现在它是一个为每个请求处理的过滤器。但是假设我们引入了自定义 @Authenticated 注释

@Target({METHOD, TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authenticated{}

我们可以使用此注释来注释不同的方法和 classes。为了使过滤器只过滤被注解的方法和 classes,我们可以引入一个 DynamicFeature 检查注解,然后只在找到注解时才注册过滤器。例如

@Provider
public class AuthenticationDynamicFeature implements DynamicFeature {

    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext configuration) {
        if (resourceInfo.getResourceMethod().isAnnotationPresent(Authenticated.class)) {
            configuration.register(new AuthenticationFilter());
            return;
        }
        
        if (resourceInfo.getResourceClass().isAnnotationPresent(Authenticated.class)) {
            configuration.register(new AuthenticationFilter());
        }
    } 
}

一旦我们注册了这个 AuthenticationDynamicFeature class,它就会使只有方法和 class 用 @Authenticated 注释的 es 会被过滤。

或者,这甚至可以在 过滤器中完成。我们可以从 AuthenticationFilter 中获取对 ResourceInfo 的引用。例如检查注释,如果不存在,则继续。

@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
    
    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext context) throws IOException {
        
        boolean hasAnnotation = false;
        if (resourceInfo.getResourceMethod().isAnnotationPresent(Authenticated.class)
                || resourceInfo.getResourceClass().isAnnotationPresent(Authenticated.class)) {
            hasAnnotation = true;
        }
        if (!hasAnnotation) return;
        
        // process authentication is annotation is present

这样我们就可以完全忘记 DynamicFeature。最好只使用DynamicFeature,我只是举个例子来演示。

但话虽如此,如果我们查看带有 RolesAllowedDynamicFeature 的第一段代码,您可以更好地理解发生了什么。它只为方法注册过滤器,并且 classes 用 @RolesAllowed@DenyAll 注释。您甚至可以重构它以在过滤器中包含所有注释逻辑而不是功能。你只有过滤器。就像我在上面的 AuthenticationFilter 示例中所做的一样。同样,这只是为了示例目的。

现在就 DynamicFeature 的注册而言,它的工作方式与注册任何其他资源 class 或提供者 class(例如您的身份验证过滤器)的方式相同。因此,无论您注册那些,只需以相同的方式注册 RolesAllowedDynamicFeature 即可。有扫描,扫描 @Path@Provider 注释。如果这是您当前正在使用的,那么只需使用 @Provider 注释功能 class 即可注册它。例如,只有一个空的 Application subclass 将导致扫描发生

@ApplicationPath("/api")
public class RestApplication extends Application {}

那么在你的Application子class中就有了显式注册。例如

@ApplicationPath("/api")
public class RestApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<>();
        classes.add(AuthenticationFilter.class);
        classes.add(RolesAllowedFeature.class);
        classes.add(SomeResource.class);
        return classes;
    }
}

请注意,执行此操作时,您将禁用任何正在进行的扫描。

所以还有一些其他事情要确保在以上所有内容都清楚之后它仍然无法正常工作。

  1. 确保您当前的 AuthenticationFilter 带有 @Priority(Priorities.AUTHENTICATION) 注释。这是为了确保在授权过滤器之前调用您的身份验证过滤器。这需要发生,因为身份验证筛选器设置安全上下文,而授权筛选器检查它。

  2. 确保您正确创建了安全上下文。授权过滤器将从 @RolesAllowed 注释中调用 SecurityContext.isUserInRole(role) 传递的角色。因此,您需要确保正确实施 isUserInRole