概要

JAX-RSリソースへのアクセスをRolesAllowedアノテーションで制限したいなぁというお話です。

:fearful: :fearful: :fearful:

2014/12/23 追記
下記手順だと、SecurityContextを上書きできていないのでまともに動作しません。
また調べて書き直します。

:fearful: :fearful: :fearful:

環境

  • Java 1.8.0 u25
  • Glassfish 4.1

仕様

http://example.com/ へのアクセスは誰にでも許し、http://example.com/secure へのアクセスを制限します(403 Forbiddenを返します) 。

403 Forbiddenのときには、自前のメッセージを出力します。

ソースコード

JAX-RSフィルタプロバイダクラス

ContainerRequestFilterを継承して作ります。ContainerRequestFilterは全リソースへのアクセス前、リソースが作成される前に実行されます。

@PreMatchingアノテーションを付けると、どのリソースを実行するのか決定する前にフィルタがかかります。

@Priorityアノテーションはフィルタを実行する順番を決めます。

肝はfilterメソッドです。ここでsetSecurityContextを呼んで、自前のSecurityContextをぶっこんでいます。

SecurityContextFilter.java

import com.sun.security.auth.UserPrincipal;
import java.security.Principal;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;

@PreMatching
@Provider
@Priority(Priorities.AUTHENTICATION)
public class SecurityContextFilter implements ContainerRequestFilter {

    private static final Logger LOG = Logger.getLogger(SecurityContextFilter.class.getName());

    UserPrincipal userPrincipal = new UserPrincipal("My name");

    SecurityContext secuityContext = new SecurityContext() {

        @Override
        public Principal getUserPrincipal() {
            LOG.log(Level.INFO, "*** getUserPrincipal");
            return userPrincipal;
        }

        @Override
        public boolean isUserInRole(String role) {
            LOG.log(Level.INFO, "*** isUserInRole: {0}", role);
            return false;
        }

        @Override
        public boolean isSecure() {
            LOG.log(Level.INFO, "*** isSecure");
            return false;
        }

        @Override
        public String getAuthenticationScheme() {
            LOG.log(Level.INFO, "*** getAuthenticationScheme");
            return null;
        }
    };

    @Override
    public void filter(ContainerRequestContext requestContext) {
        LOG.log(Level.INFO, "*** Filter invoked");

        requestContext.setSecurityContext(secuityContext);
    }
}

SecurityContextクラス

このクラスで認証しているユーザがロールを持っているかどうかを判断します。

isUserInRoleメソッド

本来であれば、下記オーバライドメソッドの中で、引数roleが認証しているユーザにマッチしているかどうかを返してあげます。
ここではお試しなのでfalseを返しています。つまり誰もアクセスできません。


        @Override
        public boolean isUserInRole(String role) {
            LOG.log(Level.INFO, "*** isUserInRole: {0}", role);
            return false;
        }

getUserPrincipal

ユーザー名とかを保持しているUserPrincipalを返してあげます。


        @Override
        public Principal getUserPrincipal() {
            LOG.log(Level.INFO, "*** getUserPrincipal");
            return userPrincipal;
        }

isSecure

コネクションがセキュアかどうか(例えばhttps)を返してあげます。


        @Override
        public boolean isSecure() {
            LOG.log(Level.INFO, "*** isSecure");
            return false;
        }

getAuthenticationScheme

認証済みの場合は、どうやって認証したか(BASIC認証、DIGEST認証、フォーム認証)を返してあげます。

とりあえずNullでおk(?)


        @Override
        public String getAuthenticationScheme() {
            LOG.log(Level.INFO, "*** getAuthenticationScheme");
            return null;
        }

リソースクラス

誰でもアクセスできるリソース

GenericResource.java

import javax.annotation.security.PermitAll;
import javax.ws.rs.Produces;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;

@Path("/")
@PermitAll
public class GenericResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getXml() {
        return "Hello JAX-RS";
    }
}

秘密のリソース

「user」ロールのひとしかアクセスできません(ということにしています)。

SecureResource.java

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.Produces;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;

@Path("secure")
@RolesAllowed("user")
public class SecureResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getXml() {
        return "Hello Secret";
    }
}

RolesAllowedアノテーションで、アクセスを許可するユーザのロールを文字列で記載します。

403 Forbidden例外を捕まえるプロバイダクラス

ForbiddenExceptionが起こったら、このクラスで捕まえさせます。

「members only」というメッセージを出力しています。

ForbiddenMapper.java

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class ForbiddenMapper implements ExceptionMapper<javax.ws.rs.ForbiddenException> {
    private static final Logger LOG = Logger.getLogger(ForbiddenMapper.class.getName());
    
    @Override
    public Response toResponse(ForbiddenException e) {
        LOG.log(Level.INFO, "Forbbidden catched.");
        
        return Response.status(Response.Status.FORBIDDEN)
                .entity("Members only")
                .type(MediaType.TEXT_PLAIN)
                .build();
    }
}

アプリケーションクラス

作成したリソースとプロバイダを登録しています。

肝はRolesAllowedDynamicFeatureです。これがないとRolesAllowdアノテーションが動きません。

ApplicationConfig.java

import java.util.Set;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;

@javax.ws.rs.ApplicationPath("/")
public class ApplicationConfig extends ResourceConfig {

    public ApplicationConfig() {
        register(RolesAllowedDynamicFeature.class);
        registerClasses(getMyClasses());
    }
    
    private Set<Class<?>> getMyClasses() {
        Set<Class<?>> resources = new java.util.HashSet<>();
        addRestResourceClasses(resources);
        
        resources.add(RolesAllowedDynamicFeature.class);
        
        return resources;
    }

    private void addRestResourceClasses(Set<Class<?>> resources) {
        resources.add(ForbiddenMapper.class);
        resources.add(GenericResource.class);
        resources.add(SecureResource.class);
        resources.add(SecurityContextFilter.class);
    }
}

pom.xml

pom.xmldependenciesセクションに追加します。


        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-server</artifactId>
            <version>2.10.4</version>
            <scope>provided</scope>
        </dependency>