GlassFish で BASIC認証を利用する (JAX-RS 編) (ja)

前回、GlassFishでBASIC認証を利用するサンプルをServlet 3.0を利用して作成しました。今回は同じ要求のアプリケーションをJAX-RSで作成してみようと思います。

JAX-RSは、以前のエントリ「web.xmlなしでJSF・JAX-WS・JAX-RSを登録する方法」で説明した通り、javax.ws.rs.core.Applicationクラスを継承することで自動登録され、web.xmlが不要になります。ところが、JAX-RSにBASIC認証などのコンテナ認証を組み合わせると、そう単純には事が進まなくなります。

まず、JAX-RSリソースのコードを示します。
AuthResource.java
package sample.auth.file;

import java.io.Serializable;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

@Path("/")
public class AuthResource implements Serializable {
	
  private static final long serialVersionUID = 1L;
	
  @GET
  @RolesAllowed({ "sergeant", "lieutenant" })
  @Path("/command")
  public String getCommand() {
    return "Wait!";
  }
	
  @POST
  @RolesAllowed("leader")
  @Path("/command")
  public String postCommand(@DefaultValue("Waiting") @FormParam("value") String value) {
    return String.format("Command %s is accepted.", value);
  }
	
  @GET
  @RolesAllowed("lieutenant")
  @Path("/operation")
  public String getOperation() {
    return "Operation Desert Storm!";
  }
}

JAX-RS では、EJB 3.x で採用されている宣言型セキュリティ、具体的にはCommon Annotations 1.0で規定されている@PermitAll@DenyAll@RolesAllowedによってアクセス制御を行うことができます。上のコードを見ていただけると分かると思いますが、JAX-RSが持つ見通しのよさに良く合致しています。

問題は、これに対するオーバーヘッドが大きいことです。特に、前回のServletの例では排除できたweb.xmlが、今回は復活してしまいます。

まずはjavax.ws.rs.core.Applicationのサブクラスを作成します。ただし、後述のようにweb.xmlにてJersey本体のサーブレットを登録することになるため、あまり効果は得られません。

AuthApplication.java
package sample.auth.file;

import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.core.Application;

public class AuthApplication extends Application {
  @Override
  public Set> getClasses() {
    Set> classes = new HashSet<>();
    classes.add(AuthResource.class);
    return classes;
  }
}

今回については、@ApplicationPathでリソースのルートを指定しても無視され、Jersey本体のサーブレットのURLマッピングが使用されます。

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">
  <display-name>authSample2</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>
  <servlet>
    <!-- ServletContainer を登録する (必須) -->
    <servlet-name>ServletContainer</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <!-- パラメータ: GlassFish では原則として不要 -->
    <!-- init-param>
      <param-name>javax.ws.rs.Application</param-name>
      <param-value>sample.auth.file.AuthApplication</param-value>
    </init-param -->
    <!-- パラメータ: @RolesAllowed 等を使用するのなら必須 -->
    <init-param>
      <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
      <param-value>com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory</param-value>
    </init-param>
    <!-- パラメータ: リソースクラスの検索範囲を限定する (任意) -->
    <!-- init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>sample.auth.file</param-value>
    </init-param -->
  </servlet>
  <servlet-mapping>
    <servlet-name>ServletContainer</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>Resource</web-resource-name>
      <url-pattern>/*</url-pattern>
      <http-method>GET</http-method>
      <http-method>POST</http-method>
      <http-method>PUT</http-method>
      <http-method>DELETE</http-method>
    </web-resource-collection>
    <auth-constraint>
      <role-name>sergeant</role-name>
      <role-name>lieutenant</role-name>
    </auth-constraint>
  </security-constraint>
  <!-- デフォルトレルムを使用するのなら省略可 -->
  <!-- login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>file</realm-name>
  </login-config -->
  <security-role>
    <role-name>sergeant</role-name>
  </security-role>
  <security-role>
    <role-name>lieutenant</role-name>
  </security-role>
  <security-role>
    <role-name>leader</role-name>
  </security-role>
</web-app>

web.xmlの記述には2点、注意点があります。
  • Jersey本体のサーブレットのinit-paramとして、名称: com.sun.jersey.spi.container.ResourceFilters、値: com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory を渡す必要があります。これがない場合はJersey側でCommons Annotationを解釈することができなくなります(結果として、@RolesAllowed等がすべて無視されることになります)。JerseyのリソースをEJB化すると回避できるかもしれませんが、実際には「許可されないクライアント」として例外がスローされ、実行することができません。
  • security-constraintの指定が必須となります。ただし、細かなアクセスコントロールはリソースに記述したアノテーションで行うため、ここでの指定は大まかなもので構いません(この例ではHTTPの主要4メソッドに対して実質全員がアクセスできるように設定しています)。
sun-web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Servlet 2.5//EN" "http://www.sun.com/software/appserver/dtds/sun-web-app_2_5-0.dtd">
<sun-web-app error-url="">
  <context-root>/authSample2</context-root>
  <security-role-mapping>
  	<role-name>leader</role-name>
  	<group-name>leader</group-name>
  </security-role-mapping>
  <security-role-mapping>
  	<role-name>lieutenant</role-name>
  	<group-name>lieutenant</group-name>
  </security-role-mapping>
  <security-role-mapping>
  	<role-name>sergeant</role-name>
  	<group-name>sergeant</group-name>
  </security-role-mapping>
  <class-loader delegate="true"/>
  <jsp-config>
    <property name="keepgenerated" value="true">
      <description>Keep a copy of the generated servlet class java code.</description>
    </property>
  </jsp-config>
</sun-web-app>
index.html (ドライバーモジュール)※別途HTTPクライアントを用意できるなら不要
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Auth Sample</title>
</head>
<body>
<h1>Auth Sample</h1>
<form id="form1" method="get" action="command">
<p>
  <input type="submit" name="send" value="show command"/>
  (for all)
</p>
</form>

<form id="form2" method="post" action="command">
<p>
  <input type="text" name="value" value=""/>
  <input type="submit" name="send" value="send command"/>
  (for leader)
</p>
</form>

<form id="form3" method="get" action="operation">
<p>
  <input type="submit" name="send" value="show operation"/>
  (for lieutenant)
</p>
</form>
</body>
</html>

今回のソースは以上の通りになります。しかし、どこか退行した感じが否めません。ではもう一歩進めて、web.xmlが不要になるよう、このソースを書き換えてみましょう。
まず、さらに前のエントリ「web.xmlを書き換えずにサーブレットを登録する」でご紹介した方法を使用して、Jersey本体のサーブレットを継承してURLパターンや初期化パラメータをアノテーションで指定できるようにします。

AuthServletContainer.java
package sample.auth.file;

import javax.servlet.annotation.HttpConstraint;
import javax.servlet.annotation.ServletSecurity;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;

import com.sun.jersey.spi.container.servlet.ServletContainer;

@ServletSecurity(
  @HttpConstraint(rolesAllowed = { "sergeant", "lieutenant" }) )
@WebServlet(
  name = "ServletContainer", 
  urlPatterns = "/*", 
  initParams = @WebInitParam(
    name = "com.sun.jersey.spi.container.ResourceFilters", 
    value = "com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory") )
public class AuthServletContainer extends ServletContainer {

  private static final long serialVersionUID = 1L;

}

これだけではweb.xmlのsecurity-roleがまだ残った状態になっています。ただし、security-roleでの指定は@DeclareRolesアノテーションで置き換えることが可能です。

AuthResource.java (modified)
package sample.auth.file;

import java.io.Serializable;

import javax.annotation.security.DeclareRoles;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

@Path("/")
@DeclareRoles({ "sergeant", "lieutenant", "leader" })
public class AuthResource implements Serializable {
	
  private static final long serialVersionUID = 1L;
	
  @GET
  @RolesAllowed({ "sergeant", "lieutenant" })
  @Path("/command")
  public String getCommand() {
    return "Wait!";
  }
	
  @POST
  @RolesAllowed("leader")
  @Path("/command")
  public String postCommand(@DefaultValue("Waiting") @FormParam("value") String value) {
    return String.format("Command %s is accepted.", value);
  }
	
  @GET
  @RolesAllowed("lieutenant")
  @Path("/operation")
  public String getOperation() {
    return "Operation Desert Storm!";
  }
}

ついでなので、sun-web.xmlをglassfish-web.xmlに置き換えてみます。中身は、少なくとも今回のサンプルの範囲では同じです。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app>
  <context-root>/authSample3</context-root>
  <security-role-mapping>
  	<role-name>leader</role-name>
  	<group-name>leader</group-name>
  </security-role-mapping>
  <security-role-mapping>
  	<role-name>lieutenant</role-name>
  	<group-name>lieutenant</group-name>
  </security-role-mapping>
  <security-role-mapping>
  	<role-name>sergeant</role-name>
  	<group-name>sergeant</group-name>
  </security-role-mapping>
  <class-loader delegate="true"/>
  <jsp-config>
    <property name="keepgenerated" value="true">
      <description>Keep a copy of the generated servlet class java code.</description>
    </property>
  </jsp-config>
</glassfish-web-app>

やり方は少々強引ですが、JAX-RSでもweb.xmlなしでBASIC認証を実装できました。