アノテーションでJAX-RSリソースへのアクセスを制限する

# 概要

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`をぶっこんでいます。

```java: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`を返しています。つまり誰もアクセスできません。

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

### getUserPrincipal

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

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

### isSecure

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

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

### getAuthenticationScheme

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

とりあえず`Null`でおｋ（？）

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

## リソースクラス

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

```java: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」ロールのひとしかアクセスできません（ということにしています）。

```java: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」というメッセージを出力しています。

```java: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`アノテーションが動きません。

```java: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.xml`の`dependencies`セクションに追加します。

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