読者です 読者をやめる 読者になる 読者になる

JAX-RS(Jersey)のFilter(RequestFilter)を使う

JAX-RS Jersey

JAX-RSのフィルタを使うが、前回書いた通り認可処理でとりあえず使うだけなので、RequestFilterが中心。

参考
Chapter 10. Filters and Interceptors

環境

  • Java8
  • Tomcat8
  • Jersey2.22.1

とりあえずFilter使う

ContainerRequestFilter

リクエストフィルタはContainerRequestFilterを使用する。

@Provider
public class LoginFilter implements ContainerRequestFilter {
    @Inject
    private SessionDto sessionDto;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("loginfilter: " + this.sessionDto.getUserDto());
        if (this.sessionDto.getUserDto() == null) {
            requestContext.abortWith(
                    Response.status(Response.Status.UNAUTHORIZED)
                            .entity("Not Logged In.")
                            .build()
            );
        }
    }
}

SessionDtoは@SessionScopedなクラス。SessionDtoにUserDtoが無ければ401(UNAUTHORIZED)を返す。
Jerseyの設定2(web.xmlとかApplicationクラスとか) - edgegram の(1)パターンで動かしているので、Filterクラスを読みこませるために@Providerを付けている。
JerseyのドキュメントにあるFilterサンプルには@Providerが付いていないが、Applicationでクラス指定している前提なのだろう。

リソースクラス

@Path("/")
@RequestScoped
public class Users {
    @Inject
    private SessionDto sessionDto;

    @Path("/user")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public UserDto user() {
        UserDto dto = this.sessionDto.getUserDto();
        return dto;
    }
}

セッションに格納されているUserDtoをJSONで返すだけ。

テスト

ブラウザで/userにアクセスすると「Not Logged In.」と表示される。
ブラウザの開発者ツールでステータスを確認するとちゃんと401になっている。
サーバーのコンソールには「loginfilter: null」が出力されている。

Name binding

次はUserDtoがセッションに格納されている時にFilterを通るか確認。
そのためにセッションにUserDtoを入れる擬似ログイン処理を追加する。

@Path("/")
@RequestScoped
public class Users {
    @Inject
    private SessionDto sessionDto;

    @Path("/user")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public UserDto user() {
        UserDto dto = this.sessionDto.getUserDto();
        return dto;
    }

    @Path("/login")
    @GET
    public Response login() {
        UserDto dto = new UserDto();
        dto.setId(1);
        dto.setName("test");
        ・・・
        this.sessionDto.setUserDto(dto);

        return Response.ok().build();
    }
}

セッションにUserDtoを格納してステータス200を返す。
これで/login → /user の順にアクセスすれば、/userはUserDtoのJSONが返ってくるはず。

なのだが、Filterは全リソースに適用されるので、このままだと/loginにもFilterがかかって401が返される。
特定のリソースだけにFilterを適用させたい、という場合にName bindingを使う。

アノテーション

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface LoggedIn {}

まず@NameBindingなアノテーションを作る。実行時に判別させるので@Retention(RetentionPolicy.RUNTIME)。

ContainerRequestFilter with NameBinding

@Provider
@LoggedIn
public class LoginFilter implements ContainerRequestFilter {
    @Inject
    private SessionDto sessionDto;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("loginfilter: " + this.sessionDto.getUserDto());
        if (this.sessionDto.getUserDto() == null) {
            requestContext.abortWith(
                    Response.status(Response.Status.UNAUTHORIZED)
                            .entity("Not Logged In.")
                            .build()
            );
        }
    }
}

Filterにアノテーションを付与する。
これでこのFilterは@LoggedInなリソースにしか効かなくなる。

リソースクラス with NameBinding

@Path("/")
@RequestScoped
public class Users {
    @Inject
    private SessionDto sessionDto;

    @Path("/user")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @LoggedIn
    public UserDto user() {
        UserDto dto = this.sessionDto.getUserDto();
        return dto;
    }

    @Path("/login")
    @GET
    public Response login() {
        UserDto dto = new UserDto();
        dto.setId(1);
        dto.setName("test");
        this.sessionDto.setUserDto(dto);

        return Response.ok().build();
    }
}

/userのメソッドだけ@LoggedIn付与。

テスト2

/loginにアクセスするとステータス200が返ってくる。
その後/userにアクセスすると「{"id":1,"name":"test"}」が表示される。

動的バインディング

NameBindingは静的Binding。動的にBindingしたい場合はDynamicFeatureを使う。
使い方はJerseyドキュメント参照。FeatureContextにFilterやInterceptorをresiterすればいい。

優先順

同じフィルタ(今回だとContainerRequestFilter)が複数ある場合に優先順を指定できる。
というか指定しなければ順番は保証されないのでするべし。
Interceptorも同様。

優先順を指定するにはFilterクラスに@Priorityを付与する。

@Provider
@LoggedIn
@Priority(Priorities.AUTHENTICATION)
public class LoginFilter implements ContainerRequestFilter {
・・・
}

value値はint型。 Prioritiesクラスにざっくりの固定値が定義されているので、それを使ってもいいし直接数値を設定してもいい。

Priorityの順序

FilterやInterceptorの処理順序はPriorityの数値順ではない

Priorityの数値が大きいほどリソースに近い処理となる。
具体的に言うと、Request系処理は数値が小さい順に処理され、Response系処理は逆に大きい順に処理される。
特にResponse処理の優先順を指定する時は注意。