JAX-RSをやってみる (17) - SecurityContext -
公式ドキュメント: 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));
}
}
ってな感じで。まだ内部的なソースな部分読んでないので色々発見があったら追記する