Best practice for REST token-based authentication with JAX-RS and Jersey

前端 未结 2 1980
借酒劲吻你
借酒劲吻你 2020-11-22 01:09

I\'m looking for a way to enable token-based authentication in Jersey. I am trying not to use any particular framework. Is that possible?

My plan is: A user signs up

2条回答
  •  猫巷女王i
    2020-11-22 02:00

    This answer is all about authorization and it is a complement of my previous answer about authentication

    Why another answer? I attempted to expand my previous answer by adding details on how to support JSR-250 annotations. However the original answer became the way too long and exceeded the maximum length of 30,000 characters. So I moved the whole authorization details to this answer, keeping the other answer focused on performing authentication and issuing tokens.


    Supporting role-based authorization with the @Secured annotation

    Besides authentication flow shown in the other answer, role-based authorization can be supported in the REST endpoints.

    Create an enumeration and define the roles according to your needs:

    public enum Role {
        ROLE_1,
        ROLE_2,
        ROLE_3
    }
    

    Change the @Secured name binding annotation created before to support roles:

    @NameBinding
    @Retention(RUNTIME)
    @Target({TYPE, METHOD})
    public @interface Secured {
        Role[] value() default {};
    }
    

    And then annotate the resource classes and methods with @Secured to perform the authorization. The method annotations will override the class annotations:

    @Path("/example")
    @Secured({Role.ROLE_1})
    public class ExampleResource {
    
        @GET
        @Path("{id}")
        @Produces(MediaType.APPLICATION_JSON)
        public Response myMethod(@PathParam("id") Long id) {
            // This method is not annotated with @Secured
            // But it's declared within a class annotated with @Secured({Role.ROLE_1})
            // So it only can be executed by the users who have the ROLE_1 role
            ...
        }
    
        @DELETE
        @Path("{id}")    
        @Produces(MediaType.APPLICATION_JSON)
        @Secured({Role.ROLE_1, Role.ROLE_2})
        public Response myOtherMethod(@PathParam("id") Long id) {
            // This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
            // The method annotation overrides the class annotation
            // So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
            ...
        }
    }
    

    Create a filter with the AUTHORIZATION priority, which is executed after the AUTHENTICATION priority filter defined previously.

    The ResourceInfo can be used to get the resource Method and resource Class that will handle the request and then extract the @Secured annotations from them:

    @Secured
    @Provider
    @Priority(Priorities.AUTHORIZATION)
    public class AuthorizationFilter implements ContainerRequestFilter {
    
        @Context
        private ResourceInfo resourceInfo;
    
        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
    
            // Get the resource class which matches with the requested URL
            // Extract the roles declared by it
            Class resourceClass = resourceInfo.getResourceClass();
            List classRoles = extractRoles(resourceClass);
    
            // Get the resource method which matches with the requested URL
            // Extract the roles declared by it
            Method resourceMethod = resourceInfo.getResourceMethod();
            List methodRoles = extractRoles(resourceMethod);
    
            try {
    
                // Check if the user is allowed to execute the method
                // The method annotations override the class annotations
                if (methodRoles.isEmpty()) {
                    checkPermissions(classRoles);
                } else {
                    checkPermissions(methodRoles);
                }
    
            } catch (Exception e) {
                requestContext.abortWith(
                    Response.status(Response.Status.FORBIDDEN).build());
            }
        }
    
        // Extract the roles from the annotated element
        private List extractRoles(AnnotatedElement annotatedElement) {
            if (annotatedElement == null) {
                return new ArrayList();
            } else {
                Secured secured = annotatedElement.getAnnotation(Secured.class);
                if (secured == null) {
                    return new ArrayList();
                } else {
                    Role[] allowedRoles = secured.value();
                    return Arrays.asList(allowedRoles);
                }
            }
        }
    
        private void checkPermissions(List allowedRoles) throws Exception {
            // Check if the user contains one of the allowed roles
            // Throw an Exception if the user has not permission to execute the method
        }
    }
    

    If the user has no permission to execute the operation, the request is aborted with a 403 (Forbidden).

    To know the user who is performing the request, see my previous answer. You can get it from the SecurityContext (which should be already set in the ContainerRequestContext) or inject it using CDI, depending on the approach you go for.

    If a @Secured annotation has no roles declared, you can assume all authenticated users can access that endpoint, disregarding the roles the users have.

    Supporting role-based authorization with JSR-250 annotations

    Alternatively to defining the roles in the @Secured annotation as shown above, you could consider JSR-250 annotations such as @RolesAllowed, @PermitAll and @DenyAll.

    JAX-RS doesn't support such annotations out-of-the-box, but it could be achieved with a filter. Here are a few considerations to keep in mind if you want to support all of them:

    • @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll on the class.
    • @RolesAllowed on the method takes precedence over @PermitAll on the class.
    • @PermitAll on the method takes precedence over @RolesAllowed on the class.
    • @DenyAll can't be attached to classes.
    • @RolesAllowed on the class takes precedence over @PermitAll on the class.

    So an authorization filter that checks JSR-250 annotations could be like:

    @Provider
    @Priority(Priorities.AUTHORIZATION)
    public class AuthorizationFilter implements ContainerRequestFilter {
    
        @Context
        private ResourceInfo resourceInfo;
    
        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
    
            Method method = resourceInfo.getResourceMethod();
    
            // @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll
            if (method.isAnnotationPresent(DenyAll.class)) {
                refuseRequest();
            }
    
            // @RolesAllowed on the method takes precedence over @PermitAll
            RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
            if (rolesAllowed != null) {
                performAuthorization(rolesAllowed.value(), requestContext);
                return;
            }
    
            // @PermitAll on the method takes precedence over @RolesAllowed on the class
            if (method.isAnnotationPresent(PermitAll.class)) {
                // Do nothing
                return;
            }
    
            // @DenyAll can't be attached to classes
    
            // @RolesAllowed on the class takes precedence over @PermitAll on the class
            rolesAllowed = 
                resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
            if (rolesAllowed != null) {
                performAuthorization(rolesAllowed.value(), requestContext);
            }
    
            // @PermitAll on the class
            if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
                // Do nothing
                return;
            }
    
            // Authentication is required for non-annotated methods
            if (!isAuthenticated(requestContext)) {
                refuseRequest();
            }
        }
    
        /**
         * Perform authorization based on roles.
         *
         * @param rolesAllowed
         * @param requestContext
         */
        private void performAuthorization(String[] rolesAllowed, 
                                          ContainerRequestContext requestContext) {
    
            if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
                refuseRequest();
            }
    
            for (final String role : rolesAllowed) {
                if (requestContext.getSecurityContext().isUserInRole(role)) {
                    return;
                }
            }
    
            refuseRequest();
        }
    
        /**
         * Check if the user is authenticated.
         *
         * @param requestContext
         * @return
         */
        private boolean isAuthenticated(final ContainerRequestContext requestContext) {
            // Return true if the user is authenticated or false otherwise
            // An implementation could be like:
            // return requestContext.getSecurityContext().getUserPrincipal() != null;
        }
    
        /**
         * Refuse the request.
         */
        private void refuseRequest() {
            throw new AccessDeniedException(
                "You don't have permissions to perform this action.");
        }
    }
    

    Note: The above implementation is based on the Jersey RolesAllowedDynamicFeature. If you use Jersey, you don't need to write your own filter, just use the existing implementation.

提交回复
热议问题