How to securing programmatic resources in a jersey

2019-07-26 23:44发布

问题:

jersey JAX-RS resources can be secured with annotations like this.

@RolesAllowed("user")
@GET
public String get() { return "GET"; }

My requirement is securing dynamically created jersey resources which I have created like this

@ApplicationPath("/")
public class MyApp extends ResourceConfig {
    public MyApp() {

        packages("com.test.res");
        Resource.Builder resourceBuilder = Resource.builder();
        resourceBuilder.path("/myresource3");

        final ResourceMethod.Builder methodBuilder = resourceBuilder.addMethod("GET");      
        methodBuilder.produces(MediaType.TEXT_PLAIN).handledBy(new TestInflector());

        Resource resource = resourceBuilder.build();
        registerResources(resource);        
        register(RolesAllowedDynamicFeature.class);
    }

}

How can I allow only "user" to access this dynamically created resource?

回答1:

Unfortunately, it appears the RolesAllowedDynamicFeature does not support resources created with programmatic API. If you look at the source for RolesAllowedDynamicFeature, you'll see that it looks for the annotations on the resource method and/or resource class to determine whether or not the resource/method should be attached to the filter that handles the authorization.

The best way I could think of is to just do the authorization in the Inflector. You can see int the source code I linked to, the filter that handles the authorization doesn't really do much. It just checks if the SecurityContext to see if the roles are allowed. You could use the logic in an Inflector. For example

public static class AuthorizationInflector
        implements Inflector<ContainerRequestContext, Response> {

    private final String[] rolesAllowed;
    private final Inflector<ContainerRequestContext, Response> delegate;

    protected AuthorizationInflector(String[] rolesAllowed,
                                     Inflector<ContainerRequestContext, Response> delegate) {
        this.rolesAllowed = (rolesAllowed != null) ? rolesAllowed : new String[] {};
        this.delegate = delegate;
    }

    @Override
    public Response apply(ContainerRequestContext context) {
        applyAuthorization(context);

        return this.delegate.apply(context);
    }


    private void applyAuthorization(ContainerRequestContext requestContext) {
        if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
            throw new ForbiddenException(LocalizationMessages.USER_NOT_AUTHORIZED());
        }

        for (final String role : rolesAllowed) {
            if (requestContext.getSecurityContext().isUserInRole(role)) {
               return;
           }
        }
        throw new ForbiddenException(LocalizationMessages.USER_NOT_AUTHORIZED());
    }

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

It looks pretty similar to the code in the RolesAllowedRequstFilter. We're handling the authorization, then just delegating the return to another Inflector. You would just use it like

final String[] rolesAllowed = {"USER"};
methodBuilder.produces(MediaType.TEXT_PLAIN_TYPE)
            .handledBy(new AuthorizationInflector(rolesAllowed, new TestInflector()));

The only real noticeable difference in behavior (with using the inflector instead of the filter) is that with the filter, you have the concept of order priority. You can see in the RolesAllowedRequestFilter that it uses a priority of Priorities.AUTHORIZATION. The reason it uses this is because the authentication filter that happens before this filter should be using the priority Priorities.AUTHENTICATION, which makes sure that authentication happens before authorization.

In the case of using the inflector, you still have this ordering, i.e. the authentication filter happens before the inflector is applied. How the behavior is different though is say you you want to implement some other filter, You want it to be performed after the authorization so you might have this

@Priority(Priorities.AUTORIZATION + 100)
class SomeFilter implements ContainerRequestFilter {}

Maybe you need to the user to be authorized. The problem when using the inflector is that it doesn't get invoked until after this filter. Which is not what you want because it is dependent on the authorization to have completed.

This is the one con of using the inflector for the authorization.

The other thing I can think of that might work (though I have yet to put all the pieces together, is to use name binding.

@NameBinding
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorization {}

methodBuilder
    .nameBinding(Authorization.class)
    .produces(MediaType.TEXT_PLAIN)
    .handledBy(new TestInflector());

@Authorization
public class AuthorizationFilter implements ContainerRequestFilter {}

You could implement the AuthorizationFilter just like the RolesAllowedRequestFilter. The thing I have yet to figure out is how to get the roles allowed from inside the filter. You obviously can't just pass it to the filter, as it needs to be scoped per resource method. I'm not sure if this can be done or not. It's something I need to play with a bit further.

For now, the only thing I can think of that I have tested and works is to just use the inflector.