Glassfish @RolesAllowed с пользовательским SecurityContext

Вопрос, который я собираюсь задать, немного сложен, и я пока не нашел ответа. Возможно, потому что я ищу не ту вещь. Но я надеюсь, что вы поможете мне в этом.

Я использовал следующий учебник для реализации пользовательского SecurityContext, который использует токены вместо базовой аутентификации пользователя / пароля.

По сути, он инициализирует и внедряет ResourceFilterFactory, который сам внедряет ResourceFilter при каждом HTTP-запросе, отправляемом приложению.

Этот ResourceFilter ищет в запросе заголовок "Аутентификация", получает его содержимое и затем аутентифицирует пользователя. Если пользователь аутентифицирован, он вводится в запрос с использованием SecurityContext.

Я преобразовал код, чтобы он работал как EJB.

Вот код:

web.xml

<init-param>  
<param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>  
<param-value>com.myapp.rest.filter.ResourceFilterFactory</param-value>
</init-param>

ResourceFilterFactory.java // Этот класс внедряет EJB SecurityContextFilter

@Named
@Stateless
@LocalBean
public class ResourceFilterFactory extends RolesAllowedResourceFilterFactory{

    @EJB(name="securityContextFilter")
    private SecurityContextFilter securityContextFilter;

    @PostConstruct
    private void init(){
        System.out.println("ResourceFilterFactory initialized");
    }

     @Override
        public List<ResourceFilter> create(AbstractMethod am) {
         //System.out.println("Creating resource filters list");
            List<ResourceFilter> filters = super.create(am);
            if (filters == null) {
                filters = new ArrayList<ResourceFilter>();
            }

            List<ResourceFilter> securityFilters = new ArrayList<ResourceFilter>(filters);
            securityFilters.clear();
            securityFilters.add(0, securityContextFilter);

            return securityFilters;
     }
}

SecurityContextFilter.java // Этот EJB вызывается при каждом HTTP-запросе, который получает сервер. Он проверяет заголовок "Аутентификация" и получает пользователя, связанного с токеном.

@Named("securityContextFilter")
@Stateless
@LocalBean
@Provider
public class SecurityContextFilter implements ResourceFilter, ContainerRequestFilter {

    protected static final String HEADER_AUTHORIZATION = "Authorization";

    @EJB
    private AuthorizationService authorizationService;

    @Override
    public ContainerRequest filter(ContainerRequest req) {
        System.out.println("Auth header: " + req.getHeaderValue(HEADER_AUTHORIZATION));
        String sessionToken =  req.getHeaderValue(HEADER_AUTHORIZATION);
        UserEntity entity = null;
        try {
            //entity = authorizationService.getParaUsingSessionToken(sessionToken);
            // removing the part that retrieves the user from the database
            ExternalUser user = new ExternalUser();
            user.setEmailAddress("lol@lol.lol");
            user.setFirstName("lol");
            user.setLastName("LOL");
            user.setRole("arole");
            req.setSecurityContext(new org.company.server.rest.filter.SecurityContextImpl(user));
        } catch (AuthenticationException e) {
            System.out.println("authentication exception");
            ExternalUser user = new ExternalUser();
            req.setSecurityContext(new org.company.server.rest.filter.SecurityContextImpl(user));
        }

        return req;
    }

    @Override
    public ContainerRequestFilter getRequestFilter() {
        return this;
    }

    @Override
    public ContainerResponseFilter getResponseFilter() {
        return null;
    }

}

SecurityContextImpl.java // Этот класс внедряется в запрос и имеет метод isUserInRole(), который должен использоваться аннотацией @RolesAllowed

public class SecurityContextImpl implements SecurityContext {


    private final ExternalUser user;

    public SecurityContextImpl(ExternalUser user) {
        //System.out.println("SecurityContext created : " + user.getFirstName());
        this.user = user;
    }

    public Principal getUserPrincipal() {
        return user;
    }

    public boolean isUserInRole(String role) {
        System.out.println("Checking access rights : " + role + " / " + this.user.getRole());
        return user.getRole().equalsIgnoreCase(role);
    }

    public boolean isSecure() {
        return false;
    }

    public String getAuthenticationScheme() {
        return SecurityContext.BASIC_AUTH;
    }
}

ExternalUser.java // Сущность, созданная и заполненная пользовательской информацией базы данных.

@XmlRootElement
public class ExternalUser implements Principal {

    private String id;
    private String firstName;
    private String lastName;
    private String emailAddress;
    private boolean isVerified;
    private String phoneNumber;
    private String professionalId;
    private String role;

    public ExternalUser() {}

    public ExternalUser(UserEntity user) {
        this.setEmailAddress(user.getEmailAddress());
        this.setFirstName(user.getFirstName());
        this.setLastName(user.getLastName());
        this.setRole(user.getRole().toString());
        this.setPhoneNumber(user.getPhoneNumber());
        this.setProfessionalId(user.getProfessionnalID());
    }
    // Getters and setters boilerplate code...
}

Наконец веб-сервис Джерси:

@Path("/account")
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
@Stateless
@LocalBean
public class UserRestService {

@Context
private SecurityContext security;

        @GET
        @Path("info")
        @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
        public Response getInfo() {
        ExternalUser user = (ExternalUser)security.getUserPrincipal();
        System.out.println("Email Address Of User : " + user.getEmailAddress());
            if (!security.isUserInRole("arole")){
                return Response.status(403).build();
            }
        return Response.ok(user).build();
        }
}

WebService работает хорошо, я получаю экземпляр ExternalUser, используя введенный SecurityContext. Но если я использую @RolesAllowed({"arole"}) аннотация, Glassfish дает мне эту ошибку:

INFO: JACC Policy Provider:Failed Permission Check: context (" org.company.app.server/org_company_app_server_internal ") , permission (" ("javax.security.jacc.EJBMethodPermission" "UserRestService" "getInfo,Local,org.company.server.rest.models.authentication.RestSession") ") 
WARNING: EJB5184:A system exception occurred during an invocation on EJB UserRestService, method: public javax.ws.rs.core.Response org.company.server.rest.services.UserRestService.getInfo()
WARNING: javax.ejb.AccessLocalException: Client not authorized for this invocation
    at com.sun.ejb.containers.BaseContainer.preInvoke(BaseContainer.java:1888)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:212)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate.invoke(EJBLocalObjectInvocationHandlerDelegate.java:88)
    at com.sun.proxy.$Proxy140.getInfoPara(Unknown Source)
    at org.company.server.rest.services.__EJB31_Generated__UserRestService__Intf____Bean__.getInfoPara(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at com.sun.jersey.spi.container.JavaMethodInvokerFactory$1.invoke(JavaMethodInvokerFactory.java:60)
    at com.sun.jersey.server.impl.model.method.dispatch.AbstractResourceMethodDispatchProvider$ResponseOutInvoker._dispatch(AbstractResourceMethodDispatchProvider.java:205)
    at com.sun.jersey.server.impl.model.method.dispatch.ResourceJavaMethodDispatcher.dispatch(ResourceJavaMethodDispatcher.java:75)
    at com.sun.jersey.server.impl.uri.rules.HttpMethodRule.accept(HttpMethodRule.java:288)
    at com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147)
    at com.sun.jersey.server.impl.uri.rules.ResourceClassRule.accept(ResourceClassRule.java:108)
    at com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147)
    at com.sun.jersey.server.impl.uri.rules.RootResourceClassesRule.accept(RootResourceClassesRule.java:84)
    at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1469)
    at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1400)
    at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1349)
    at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1339)
    at com.sun.jersey.spi.container.servlet.WebComponent.service(WebComponent.java:416)
    at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:537)
    at com.sun.jersey.spi.container.servlet.ServletContainer.doFilter(ServletContainer.java:895)
    at com.sun.jersey.spi.container.servlet.ServletContainer.doFilter(ServletContainer.java:843)
    at com.sun.jersey.spi.container.servlet.ServletContainer.doFilter(ServletContainer.java:804)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:256)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:217)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:279)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
    at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:655)
    at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:595)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:161)
    at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:331)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:231)
    at com.sun.enterprise.v3.services.impl.ContainerMapper$AdapterCallable.call(ContainerMapper.java:317)
    at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:195)
    at com.sun.grizzly.http.ProcessorTask.invokeAdapter(ProcessorTask.java:849)
    at com.sun.grizzly.http.ProcessorTask.doProcess(ProcessorTask.java:746)
    at com.sun.grizzly.http.ProcessorTask.process(ProcessorTask.java:1045)
    at com.sun.grizzly.http.DefaultProtocolFilter.execute(DefaultProtocolFilter.java:228)
    at com.sun.grizzly.DefaultProtocolChain.executeProtocolFilter(DefaultProtocolChain.java:137)
    at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:104)
    at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:90)
    at com.sun.grizzly.http.HttpProtocolChain.execute(HttpProtocolChain.java:79)
    at com.sun.grizzly.ProtocolChainContextTask.doCall(ProtocolChainContextTask.java:54)
    at com.sun.grizzly.SelectionKeyContextTask.call(SelectionKeyContextTask.java:59)
    at com.sun.grizzly.ContextTask.run(ContextTask.java:71)
    at com.sun.grizzly.util.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:532)
    at com.sun.grizzly.util.AbstractThreadPool$Worker.run(AbstractThreadPool.java:513)
    at java.lang.Thread.run(Thread.java:722)

WARNING: StandardWrapperValve[default]: PWC1406: Servlet.service() for servlet default threw exception
javax.ejb.AccessLocalException: Client not authorized for this invocation
    at com.sun.ejb.containers.BaseContainer.preInvoke(BaseContainer.java:1888)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:212)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate.invoke(EJBLocalObjectInvocationHandlerDelegate.java:88)
    at com.sun.proxy.$Proxy140.getInfoPara(Unknown Source)
    at org.company.server.rest.services.__EJB31_Generated__UserRestService__Intf____Bean__.getInfoPara(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    ...

Я думаю, что мне нужно настроить Glassfish, чтобы он использовал созданный мной SecurityContextImpl. Я не знаю, почему не работает, поскольку он правильно введен, и я могу вызвать его методы в своем коде.

Я мог бы просто использовать isUserInRole() метод вручную, но я бы просто избежал проблемы вместо того, чтобы столкнуться с ней. Извините за длинный пост, но теперь я думаю, что у вас есть вся информация, необходимая, чтобы помочь мне в этом. Спасибо заранее за вашу помощь.

Эмерик

2 ответа

Решение

Хорошо, я только что закончил эту часть (на самом деле все части, за исключением EmailGateway).

Во-первых, я говорю спасибо Иану Портеру за его работу - ее высокое качество. И я прошу прощения за то, что по ошибке назвал его просто "Портер".

Что-то происходит с форматированием кода Stackru, так что имейте в виду, что некоторый код обрабатывает поля кода.

Для вашей проблемы я сделал это:

  1. web.xml - я не использую кроме Faces
  2. Вместо web.xml и для регистрации различных провайдеров я создал класс AuthApplicationConfig.java

    @ApplicationPath("rest")
    public class AuthApplicationConfig extends Application {
    
        @Override
        public Set<Class<?>> getClasses() {
            Set<Class<?>> resources = new java.util.HashSet<>();
    
            // REST resources
            resources.add(HealthCheckResource.class);
            resources.add(PasswordResource.class);
            resources.add(UserResource.class);
            resources.add(VerificationResource.class);
    
            // Filters (Auth)
            resources.add(RolesAllowedDynamicFeature.class);
            resources.add(SecurityContextFilter.class);
    
            // Misc
            resources.add(GenericExceptionMapper.class);
            return resources;
        }
    }
    

См. https://java.net/jira/browse/JERSEY-1634 чтобы узнать, почему вам нужно зарегистрировать классы, даже если они помечены @Provider

  1. ResourceFilterFactory.java - я не использовал
  2. SecurityContextFilter.java немного отличается:

    @Provider
    @Priority(Priorities.AUTHENTICATION)    // So it comes in before org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
    
    public class SecurityContextFilter implements ContainerRequestFilter {
        @Inject
        Logger logger;
    
        protected static final String HEADER_AUTHORIZATION = "Authorization";
    
        protected static final String HEADER_DATE = "x-java-rest-date";
    
        protected static final String HEADER_NONCE = "nonce";
    
        private AuthorizationService authorizationService;
    
        ApplicationConfig config;
    
        @Inject
        public SecurityContextFilter(UserRepository userRepository,
                UserService userService, ApplicationConfig config) {
            delegateAuthorizationService(userRepository, userService, config);
            this.config = config;
    
        }
    
        /**
         * If there is an Authorisation header in the request extract the session
         * token and retrieve the user
         * 
         * Delegate to the AuthorizationService to validate the request
         * 
         * If the request has a valid session token and the user is validated then a
         * user object will be added to the security context
         * 
         * Any Resource Controllers can assume the user has been validated and can
         * merely authorize based on the role
         * 
         * Resources with @PermitAll annotation do not require an Authorization
         * header but will still be filtered
         * 
         * @param request
         *            the ContainerRequest to filter
         * 
         */
        @Override
        public void filter(ContainerRequestContext requestContext)
                throws IOException {
            System.out.println("SecurityContextFilter / filter("+printContainerRequestContext(requestContext)+")");
            String authToken = requestContext.getHeaderString(HEADER_AUTHORIZATION);
            String requestDateString = requestContext.getHeaderString(HEADER_DATE);
            String nonce = requestContext.getHeaderString(HEADER_NONCE);
            AuthorizationRequestContext context = new AuthorizationRequestContext(
                    requestContext.getUriInfo().getPath(),
                    requestContext.getMethod(), requestDateString, nonce, authToken);
            ExternalUser externalUser = authorizationService.authorize(context);
            requestContext
                    .setSecurityContext(new SecurityContextImpl(externalUser));
            System.out.println(String.format(" END OF SecurityContextFilter / filter - AuthorizationRequestContext: %s, externalUser:%s", context,externalUser));
        }
    
        private String printContainerRequestContext(ContainerRequestContext requestContext) {
            return String.format("[ContainerRequestContext:%s]", requestContext);
        }
    
        /**
         * Specify the AuthorizationService that the application should use
         * 
         * @param userRepository
         * @param userService
         * @param config
         */
        private void delegateAuthorizationService(UserRepository userRepository,
                UserService userService, ApplicationConfig config) {
            System.out.println("SecurityContextFilter - requireSignedRequests?"+config.requireSignedRequests());
            if (config.requireSignedRequests()) {
                this.authorizationService = new RequestSigningAuthorizationService(
                        userRepository, userService, config);
            } else {
                this.authorizationService = new SessionTokenAuthorizationService(
                        userRepository);
            }
        }
    
        @Inject
        public void setConfig(ApplicationConfig config) {
            this.config = config;
        }
    }
    
  3. SecurityContextImpl такой же, как определил Iain:

    public class SecurityContextImpl implements SecurityContext {
    
        private final ExternalUser user;
    
        public SecurityContextImpl(ExternalUser user) {
            this.user = user;
        }
    
        @Override
        public Principal getUserPrincipal() {
            return user;
        }
    
        @Override
        public boolean isUserInRole(String role) {
            if(role.equalsIgnoreCase(Role.anonymous.name())) {
                 return true;
            }
            if(user == null) {
                throw new InvalidAuthorizationHeaderException();
            }
            System.out.println(String.format("SecurityContextImpl / isUserInRole - role:%s, user:%s", role, user));
            return user.getRole().equalsIgnoreCase(role);
        }
    
        @Override
        public boolean isSecure() {
            return false;
        }
    
        @Override
        public String getAuthenticationScheme() {
            return SecurityContext.BASIC_AUTH;
        }
    }
    
  4. UserResource, как определил Iain, похож на ваш UserRestService:

    @Path("/user")
    // @Component
    @Produces({ MediaType.APPLICATION_JSON })
    @Consumes({ MediaType.APPLICATION_JSON })
    @RequestScoped
    public class UserResource {
        // A Social thing that is not needed
        // private ConnectionFactoryLocator connectionFactoryLocator;
        @Inject
        Logger logger;
    
        @Inject
        protected UserService userService;
    
        @Inject
        protected VerificationTokenService verificationTokenService;
    
        @Inject
        protected EmailServicesGateway emailServicesGateway;
    
        @Context
        protected UriInfo uriInfo;
    
        // @Inject
        // protected ApplicationConfig config;
    
        // @Autowired
        // public UserResource(ConnectionFactoryLocator connectionFactoryLocator) {
        // this.connectionFactoryLocator = connectionFactoryLocator;
        // }
    
        @PermitAll
        @POST
        public Response signupUser(CreateUserRequest request) {
            AuthenticatedUserToken token = userService.createUser(request, Role.authenticated);
            verificationTokenService.sendEmailRegistrationToken(token.getUserId());
            URI location = uriInfo.getAbsolutePathBuilder().path(token.getUserId()).build();
            return Response.created(location).entity(token).build();
        }
    
        @RolesAllowed("admin")
        @Path("{userId}")
        @DELETE
        public Response deleteUser(@Context SecurityContext sc, @PathParam("userId") String userId) {
            ExternalUser userMakingRequest = (ExternalUser) sc.getUserPrincipal();
            userService.deleteUser(userMakingRequest, userId);
            return Response.ok().build();
        }
        ...
    

Как насчет вашего web.xml?

Вы уже поместили эти фильтры в web.xml?

<init-param>
       <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
       <param-value>com.yourcompany.filter.SecurityFilter</param-value>
   </init-param>
    <init-param>
        <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
        <param-value>com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory</param-value>
    </init-param>
Другие вопросы по тегам