JAX-RSをやってみる (17) - SecurityContext -

2014-10-06T00:00:00+00:00 Java JAX-RS

公式ドキュメント: https://jersey.java.net/documentation/latest/security.html

サーブレットシステムとかで使われるようなPrincipalを使ったセキュリティロールをアノテーションで補助する機能を持つような感じかと。言ってる事わかりづらいと思うのでさっさと本題に入る

web.xml

※jettyで確認

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <servlet>
        <servlet-name>jersey</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>sample.SampleApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>jersey</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

    <security-constraint>
        <web-resource-collection>
            <url-pattern>/sample/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>*</role-name>
        </auth-constraint>
    </security-constraint>

    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>basic_realm</realm-name>
    </login-config>

</web-app>

ってな感じで<security-constraint>周りを設定。

検証で使うjettyな設定ファイルを作る

jetty-env.xmlを作る

<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.mortbay.org/configure.dtd">
<Configure class="org.mortbay.jetty.webapp.WebAppContext">
    <Get name="securityHandler">
        <Set name="userRealm">
            <New class="org.mortbay.jetty.security.HashUserRealm">
                <Set name="name">default</Set>
                <Set name="config">realm.properties</Set>
            </New>
        </Set>
    </Get>
</Configure>

realm.propertiesで指定しているので

hoge:hoge,a
fuga:fuga,a,b

な感じで「ユーザー名:パスワード,ロール名」みたいに指定する

SampleApplication.java

package sample;

import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;

@ApplicationPath("/")
public class SampleApplication extends ResourceConfig {
    public SampleApplication() {
        register(RolesAllowedDynamicFeature.class);
        packages("sample");
    }
}

RolesAllowedDynamicFeatureに関しては後ほど

Sample.java

package sample;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;

@Path("/sample")
public class Sample {

    @GET
    public String index() {
        return "hoge";
    }

    @Path("/a")
    @RolesAllowed("a")
    @GET
    public String a(@Context SecurityContext context) {
        return "A: " + context.getUserPrincipal().getName();
    }

    @Path("/b")
    @RolesAllowed("b")
    @GET
    public String b() {
        return "B";
    }
}

っていうような感じでリソースアクションとなる部分などに@RolesAllowedアノテーションを使ってリソースにアクセス出来るロールを持つユーザーを限定する事が出来る。この機能自体は上記のRolesAllowedDynamicFeatureで提供されるので、それが有効になっている状態で特定のロールを持たないユーザーがアクセスしようとするとHTTP/403エラーになる。つまりaリソースは上記で設定したユーザーロールだとhogeとfugaユーザー両方でアクセス出来るけど、bリソースに関してはhogeユーザーでアクセスすると403エラーになる、理由は書いた通り特定のロールを持たないユーザーでアクセスは出来ない

あと@PermitAllっていうのもある。又、引数などにSecurityContextを持つ事が出来るがログイン処理がされていない場合などにはgetUserPrincipalはnullが返ってくる

テストを書く

SecurityContextを伴う場合のテストはContainerRequestFilterを使ってContainerRequestContextにsetSecurityContextを使ってやれば良いらしい(公式のgithubになるテストによると)

package sample;

import java.io.IOException;
import java.security.Principal;

import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;

import org.glassfish.hk2.api.TypeLiteral;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.internal.inject.ReferencingFactory;
import org.glassfish.jersey.internal.util.collection.Ref;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class SampleTest extends JerseyTest {

    public static class SampleSecurityContext implements SecurityContext {
        @Override
        public Principal getUserPrincipal() {
            System.out.println("getUserPrincipal");
            // SecurityContext.getUserPrincipalを使う場合にはインスタンスを返しておく
            return new Principal() {
                @Override
                public String getName() {
                    return "hoge";
                }
            };
        }

        @Override
        public boolean isUserInRole(String role) {
            // 利用するリソースが必要とするロールとユーザーロールが一致しているかを検証
            return "a".equals(role);
        }

        @Override
        public boolean isSecure() {
            return false;
        }

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

    // don`t forget
    @PreMatching
    public static class SecurityContainerRequestFilter implements ContainerRequestFilter {

        @Inject
        Ref<SecurityContext> securityContextRef;

        @Override
        public void filter(ContainerRequestContext requestContext)
            throws IOException {
            requestContext.setSecurityContext(securityContextRef.get());
        }
    }

    @Override
    protected ResourceConfig configure() {
        final SecurityContext sc = new SampleSecurityContext();

        return new ResourceConfig()
            .register(new AbstractBinder() {
                @Override
                protected void configure() {
                    bindFactory(
                        ReferencingFactory.<SecurityContext>referenceFactory(sc)
                    ).to(new TypeLiteral<Ref<SecurityContext>>() {});
                }
            })
            .register(SecurityContainerRequestFilter.class)
            .register(RolesAllowedDynamicFeature.class)
            .packages("sample");
    }

    @Test
    public void test_index() {
        Response response = target("/sample")
            .request()
            .get();

        assertThat(response.getStatus(), is(200));
    }

    @Test
    public void test_a() {
        Response response = target("/sample/a")
            .request()
            .get();

        assertThat(response.getStatus(), is(200));
    }

    @Test
    public void test_b() {
        Response response = target("/sample/b")
            .request()
            .get();

        // fail: status is 403
        assertThat(response.getStatus(), is(200));

    }
}

ってな感じで。まだ内部的なソースな部分読んでないので色々発見があったら追記する

JAX-RSをやってみる (18) - OAuth1Provider - HttpsURLConnection TrustManager