Jersey MVCのレスポンスのContent-Typeが「*/*」になる問題対策

詳細は後ほど追記します。

ContianerResponseFilterだと何故かできなかった・・・。

Jersey MVCは、MessageBodyWriterの中でContent-Typeを上書きしています。

ContianerResponseFilterはMessageBodyWriterよりも前に実行されるので、そりゃあ効かないですね・・・。

なので、必ずWriterInterceptorの「後処理」として書く必要があります。

MessageBodyWriterでなぜ「*/*」に上書きされるのかは、現在ソースを読んで調査中です。

package com.example.rest.interceptor;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import java.io.IOException;

@Provider
public class ContentTypeWriterInterceptor implements WriterInterceptor {
    @Override
    public void aroundWriteTo(WriterInterceptorContext context) 
            throws IOException, WebApplicationException {
        // デバッグログ
        System.out.println("======== BEFORE : " + context.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
        // レスポンスへの書き込み実行(必須)
        context.proceed(); 
        // デバッグログ
        System.out.println("======== AFTER : " + context.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
        // レスポンスヘッダーに "Content-Type: text/html"を上書きする
        MultivaluedMap<String, Object> headers = context.getHeaders();
        headers.putSingle(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML);
    }
}

Payara 161.1 で @Transactional のバグが修正されました!

この記事は、半分ポエムです。

最初のきっかけ

下記の@opengl_8080さんの記事でした。

JavaEE使い方メモ(JTA) - Qiita

CDIビーンのメソッドに@Transactionalアノテーションを付加しても、非チェック例外発生時にロールバックされない、というバグがあるのこと。

JTAの仕様としては、チェック例外ならばコミット、非チェック例外ならばロールバックされます)

早速、自分でも試してみたのですが、やっぱりロールバックされませんでした。

はじめてのISSUE報告(2015/11/1)

僕は、トランザクション管理はJava EEの要だと思っていますので、PayaraのGitHubにISSUEを書きました。

英語あんまり得意じゃないですが・・・(^^;

テスト用プログラム(上記の@opengl_8080さんの記事を参考に作りました)

GitHub - MasatoshiTada/TransactionalSample-Doma

ISSUE

Payara does NOT rollback when RuntimeException occurs in CDI @Transactional method using JDBC · Issue #505 · payara/Payara · GitHub

いま読み返しても、ひどい英語だ・・・。でも、もうこれは気合いでした。

Payaraの中の人から反応が!(2015-11-15)

Payaraの中の人たちは、すぐに反応してくれました。

テスト用プログラムのおかげで再現性がすぐに認められ、バグ修正も速やかに行われました!

PAYARA-510 fixes 505 ensures resources are enlisted into the transaction by smillidge · Pull Request #524 · payara/Payara · GitHub

最初にバグを見つけたのは@opengl_8080さんですし、僕はISSUEを書いただけです。

しかし、僕は現役のエンジニアではなく、僕が作ったプログラムが世の中で動くわけではないので、

本当に、ほんのちょっとでもPayaraに貢献できたことが、本当に嬉しかったんです。

修正されたPayaraがリリース!しかし・・・(2016-01)

上記の修正は、Payara 4.1.1.161に含まれました。

ウキウキして早速試してみました。

すると・・・

直ってないやん(--;)

TxTypeREQUIREDの挙動は直っていたんですが、REQUIRES_NEWの挙動は直っていませんでした。

再びISSUE報告(2016-02)

数日後、改めてISSUEを書きました。

#505 is not fixed when TxType is REQUIRES_NEW · Issue #667 · payara/Payara · GitHub

すると、やはりすぐに反応が返ってきました。

前回のことを覚えてくれていたのか、すぐに修正も行われました。

ちなみに、2回とも修正対応してくれたsmillidgeさん、Payaraを開発している英国C2B2社の創業者の方だそうです。

もし今年のJava Day Tokyoとかにいらっしゃったりすれば、是非お話してみたいなあ・・・。

リリースが思わぬ速さで!(2016-03)

Payaraは通常、四半期に1回のリリースなんですが、たまに四半期の間でもパッチ版がリリースされます。

僕は、「修正版が出るのは次の四半期かなー」と思っていたんですが、つい先日、パッチ版のPayara 4.1.1.161.1がリリースされました!

(「1」が多いので、以降は「Payara 161.1」と書きます)

このリリースに、上記バグの修正が含まれています。

このバグ修正の何が嬉しいのか?

JPA以外のORマッパーをPayara上で使いつつ、かつ@Transactionalでトランザクション管理を行うことができます。

(上記バグは、以前からJPAでは発生しませんでした)

最近、「100%Java EE標準」にこだわらないのであれば、JPA以外のORマッパー(Doma・MyBatis・jOOQ・DBFluteなど)を使う選択肢もアリだと思っています。

このような構成でも、安心してPayaraを使うことができます。

最後に

このバグを見つけてくれた@opengl_8080さんには、本当に感謝です。

繰り返しますが、僕はISSUEを書いただけです。

でも、将来的には、自分でバグ修正までしてプルリクエストを送れるくらいになりたいなあ・・・。

Jersey Test + MockitoでJAX-RSリソースクラスの単体テスト

Jersey TestでJAX-RSリソースクラスの単体テストするときに、Mockitoでモックのビジネスロジックを差し込みます。

環境

  • Payara Web ML 4.1.1.161
  • Jersey 2.22.1

pom.xml

  • JUnit
  • Jersey Test
  • Jersey Testの実行環境
  • Mockito

が必要です。

jersey-test-framework-provider-grizzly2を依存性に含めると、Jersey Testと実行環境が両方入ります。

    <properties>
        <jersey.version>2.22.1</jersey.version>
        <jee-webapi.version>7.0</jee-webapi.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>${jee-webapi.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>1.10.19</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

テスト対象のリソースクラスなど

package sample;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/api")
public class MyApplication extends Application {
     // 中身は空
}
package sample;

import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.MediaType;

@Path("/hello")
@Produces(value = MediaType.APPLICATION_JSON)
@RequestScoped
public class HelloResource {

    // テスト時はここをモックに差し替える
    @Inject
    private HelloLogic helloLogic;

    @GET
    public Response getAll() throws Exception {
        List<HelloDto> list = helloLogic.selectAll();
        return Response.ok(list).build();
    }
}
package sample;

public class HelloDto {
    private String name;
    // setter/getter/コンストラクタ省略
}
package sample;

import java.util.List;
import javax.enterprise.context.RequestScoped;

@RequestScoped
public class HelloLogic {
    public List<HelloDto> selectAll() {
        // 本来は何らかの複雑な処理があると考えてください
        return Arrays.asList(new HelloDto("AAA"), new HelloDto("BBB"), new HelloDto("CCC"));
    }
}

テストクラスの作成

package sample;

import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.utilities.Binder;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.Mockito.*;

public class HelloResourceTest extends JerseyTest {

    /**
      * HelloLogicのモックのファクトリークラス
      */
    private static class MockHelloLogicFactory implements Factory<HelloLogic> {
        @Override
        public HelloLogic provide() {
            // mock() / when() / thenReturn() はMockitoのメソッド
            HelloLogic mockLogic = mock(HelloLogic.class);
            when(mockLogic.selectAll()).thenReturn(Arrays.asList(
                    new HelloDto("仮のA"),
                    new HelloDto("仮のB")
            ));
            return mockLogic;
        }

        @Override
        public void dispose(ProductQueryDao instance) {
            // 何もしない
        }
    }

    @Override
    protected Application configure() {
        Binder binder = new AbstractBinder() {
            @Override
            protected void configure() {
                // bindFactory() + to() でモックに差し替え
                bindFactory(MockHelloLogicFactory.class)
                        .to(HelloLogic.class);
            }
        };
        return new ResourceConfig()
                .packages(true, MyApplication.class.getPackage().getName())
                .register(binder);
    }

    @Test
    public void 全件取得でき件数が2件() {
        List<HelloDto> list = target("hello")
                .request()
                .get(new GenericType<List<HelloDto>>(){});
        assertThat(list.size(), is(2));
    }
}

ポイントは、ファクトリークラスを作成することと、bindFactory()メソッドを利用すること。

下記の@backpaper0さんの資料にあるbind()メソッドは、Javadoc

Does NOT bind the service type itself as a contract type.

とあり、Mockitoで作ったmockLogicを引数に指定すると、例外でテストがこけた。

bind()の引数はインタフェースのみ指定可能なのかも?

参考資料

Mockito 初めの一歩 - Qiita

java - Mocking EJB's with Mockito and Jersey Test Framework - Stack Overflow

http://backpaper0.github.io/ghosts/jaxrs-getting-started-and-practice.html#/12/8

AbstractBinder (HK2 API module 2.2.0-b11-SNAPSHOT API)

Factory (HK2 API module 2.2.0-b11-SNAPSHOT API)

指定したフォルダ配下の全.javaファイルのタブをスペースに一括変換する

$ find . -name "*.java" -exec perl -i.bak -pe 's/\t/    /g' "{}" \;

こうするとタブがスペース4つに変換はされるが、「Xxx.java.bak」というファイルが大量にできるので、これを一括削除。

$ find . -name "*.bak" -delete

参考URL

java - Change all tabs with whitespaces in IntelliJ for 10K+ classes - Stack Overflow

MacWiki - UNIXの基本コマンド

Jersey MVCでレルム認証する(Jerseyも基本的には方法は同じ!)

Jersey MVCでレルム認証する(Jerseyも基本的には方法は同じ!)

Java EEの認証・認可機能といえばレルムですね。

今回は、Jersey MVCでレルムを使ってみました。MVCじゃないJerseyでも方法は基本的に同じです。

ソースコードGitHubにアップしています。

https://github.com/MasatoshiTada/JerseyMVC_JDBCRealm

やりたいこと

  • ログイン機能をレルムで作る。
  • 未ログイン時に、ログイン時でないと見れない画面のURLを直接指定した場合、401エラー画面に強制遷移する。
  • 複数のロールを用意して、権限のないユーザーが管理者ロール専用の画面に遷移しようとしたら、403エラー画面に強制遷移する。

GlassFishのレルム設定

Java EEでレルムを利用する際は、アプリケーションサーバー側に設定が必要です。

今回、僕はPayara Web ML 4.1.1.154を使っています。

Payaraの設定方法はGlassFishと全く一緒です。寺田さんの下記のブログに手順が詳しく書いてあります。

http://yoshio3.com/2013/12/10/glassfish-jdbc-realm-detail/

依存ライブラリの追加

pom.xmlに依存性を追加します。最低限、javaee-web-apiとjersey-mvc-jsp(MVCでないならばjersey-server)があればOK。

それ以外に、特別な依存性は必要ありません。

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- Jersey MVC + Jersey MVC JSP -->
        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-mvc-jsp</artifactId>
            <scope>provided</scope>
            <!-- 不要な依存性を除外 -->
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

ResourceConfigサブクラスの作成

ここが1つ目のポイントです。

RolesAllowedDynamicFeatureクラスの登録が必要になります。

これが無いと、リソースメソッドに@RolesAllowed付けても無視されます。

これは、JerseyでもJersey MVCでも同じです。

package com.example.rest;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.server.mvc.jsp.JspMvcFeature;

@ApplicationPath("api")
public class MyResourceConfig extends ResourceConfig {
    public MyResourceConfig() {
        register(JspMvcFeature.class);
        property(JspMvcFeature.TEMPLATE_BASE_PATH, "/WEB-INF/views/");
        packages(true, this.getClass().getPackage().getName());
        register(RolesAllowedDynamicFeature.class); // コレが必要!
    }
}

web.xmlの作成

レルムの利用に必要な設定を記述します。

レルムでの認証方法は、BASIC認証・FORM認証・DIGEST認証・SSL相互認証の4種類があります。

最初は、Java EE標準のFORM認証を使っていたのですが、FORM認証は基本的にはサーブレットJSPのことしか考慮されていないものなので、Jersey MVCだと使いづらかったです(^^;

なので方針を変えて、設定は「BASIC」にしておいて、ログイン処理自体は自前で実装(後述)することにしました。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

    <welcome-file-list>
        <welcome-file>index</welcome-file>
    </welcome-file-list>

    <security-constraint>
        <!-- 保護対象のURL -->
        <web-resource-collection>
            <web-resource-name>Sample System</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <!-- HTTPS化 -->
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <login-config>
        <auth-method>BASIC</auth-method>
        <!-- Payaraに設定したレルム名 -->
        <realm-name>jerseyMvc</realm-name>
    </login-config>

    <!-- ロールの一覧 -->
    <security-role>
        <description/>
        <role-name>ADMINISTRATOR</role-name>
    </security-role>
    <security-role>
        <description/>
        <role-name>MANAGER</role-name>
    </security-role>
    <security-role>
        <description/>
        <role-name>OPERATOR</role-name>
    </security-role>
</web-app>

ウェルカムファイルからログイン画面にリダイレクトするサーブレットの作成

web.xmlにウェルカムファイルを指定しましたが、Jersey MVCコントローラー経由のパスは指定できません。

なので、一旦サーブレットでリクエストを受け取って、ログイン画面にリダイレクトします。

package com.example.servlet;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/index")
public class RedirectServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.sendRedirect("./api/login");
    }
}

ログイン画面の作成

FORM認証ではないので、j_security_checkj_usernamej_passwordは必要ありません。いたって普通のJSPです。

今回は、Jersey MVC相対パスを使うので、このJSPはsrc/main/webapp/WEB-INF/views/com/example/rest/resource/LoginResourceフォルダに作成します。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>ログイン画面</title>
    </head>
    <body>
        <c:if test="${not empty param['retry']}">
            <p>ログインIDまたはパスワードが違います。</p>
        </c:if>
        <form action="./login" method="post">
            ログインID:<input type="text" name="loginId"><br>
            パスワード:<input type="password" name="password"><br>
            <input type="submit" value="ログイン">
        </form>
    </body>
</html>

ログイン用コントローラーの作成

先ほどのJSPからのリクエストパラメータで、ログインIDとパスワードを受け取り、ログイン処理を行います。

ログイン処理は、HttpServletRequest#login()メソッドを使います。

あんまりサーブレットAPIに依存させたくないんですが、レルムでログインするには仕方がないですね・・・。

ログインに成功したらインデックス画面にリダイレクトし、失敗したらログイン画面に戻ります。

package com.example.rest.resource;

import java.net.URI;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import org.glassfish.jersey.server.mvc.Viewable;

@Path("login")
public class LoginResource {

    @Context
    private SecurityContext securityContext;
    @Context
    private UriInfo uriInfo;
    @Context
    private HttpServletRequest httpServletRequest;

    @GET
    public Viewable index() {
        return new Viewable("login");
    }

    @POST
    public Response login(@FormParam("loginId") String loginId,
            @FormParam("password") String password) {
        // 未ログインならばログイン処理を行う
        if (securityContext.getUserPrincipal() == null) {
            System.out.println("ログイン処理開始...");
            try {
                // コンテナにログイン
                httpServletRequest.login(loginId, password);
                System.out.println("ログイン処理成功!!");
            } catch (ServletException ex) {
                System.out.println("ログイン失敗");
                // ログイン失敗時はログイン画面に戻る
                URI loginPage = uriInfo.getBaseUriBuilder()
                        .path(this.getClass())
                        .queryParam("retry", Boolean.TRUE)
                        .build();
                return Response.status(Response.Status.SEE_OTHER)
                        .location(loginPage)
                        .build();
            }
        }
        // ログイン成功時はインデックス画面に遷移
        URI indexPage = uriInfo.getBaseUriBuilder()
                .path(SecretResource.class)
                .path("index")
                .build();
        return Response.status(Response.Status.FOUND)
                .location(indexPage)
                .build();
    }

}

ログイン後のインデックス画面

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデックス画面</title>
    </head>
    <body>
        <h1>ようこそ、${pageContext.request.userPrincipal.name}さん!</h1>
        <a href="./admin">管理者専用画面へ</a>
        <hr>
        <form action="${pageContext.request.contextPath}/api/logout" method="post">
            <input type="submit" value="ログアウト">
        </form>
    </body>
</html>

管理者専用画面

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>管理者専用画面</title>
    </head>
    <body>
        <h1>管理者専用画面です。</h1>
        <a href="./index">インデックス画面へ</a>
    </body>
</html>

ログイン後の画面用コントローラーの作成

ここで、セキュリティアノテーションを使います。

インデックス画面は全ロールに許可するために@PermitAll、管理者専用画面は管理者のみ許可するために@RolesAllowedを付加します。

@Authenticatedは自作アノテーションなのですが、後ほど説明します。

package com.example.rest.resource;

import com.example.rest.filter.binding.Authenticated;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import org.glassfish.jersey.server.mvc.Viewable;

@Path("secret")
@Authenticated // ログインチェック用のフィルターが適用される。メソッドに個別につけてもOK
public class SecretResource {

    @GET
    @Path("index")
    @PermitAll // 全ロールに許可
    public Viewable index() {
        return new Viewable("index");
    }

    @GET
    @Path("admin")
    @RolesAllowed("ADMINISTRATOR") // 管理者のみ許可
    public Viewable admin() {
        return new Viewable("admin");
    }

}

ログインチェック用のフィルター作成

サーブレットフィルターではなく、JAX-RSフィルターです。

まず、このフィルターを適用する部分を決めるためのアノテーションを作成します。

package com.example.rest.filter.binding;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.ws.rs.NameBinding;

@NameBinding // これがポイント!
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authenticated {}

@NameBindingを付加しているのがポイントです。

そして、フィルターにはこのアノテーションを付加します。

package com.example.rest.filter;

import com.example.rest.filter.binding.Authenticated;
import java.io.IOException;
import javax.annotation.Priority;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;

@Provider
@Authenticated // このアノテーションが付いているメソッドにのみ、このフィルターが適用される
@Priority(Priorities.AUTHENTICATION) // 優先度をAUTHENTICATION(=1000)にする
public class AuthenticationFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        SecurityContext securityContext = requestContext.getSecurityContext();
        // ログインしてなかったら401例外をスロー
        if (securityContext.getUserPrincipal() == null) {
            throw new NotAuthorizedException("not login");
        }
    }

}

もう1つのポイントは、@Priority(Priorities.AUTHENTICATION)です。

JAX-RSフィルターは、複数のフィルターが定義されていた場合の実行順序を指定できます。

Priorities.AUTHENTICATIONは「1000」という整数値です。

@Priorityに指定した整数値が小さいものから先に、フィルターが実行されます。

最初に設定クラスを作成した時、RolesAllowedDynamicFeatureクラスを登録しました。

このクラスを登録すると、@RolesAllowedによる権限チェックを行うJAX-RSフィルターも登録されます。

(この権限チェックフィルターは、権限なしの場合は403例外(ForbiddenException)をスローします。)

で、この権限チェックフィルターのPriorityは、Priorities.AUTHORIZATION(=2000)が指定されているんですね。

ログインチェック(=認証)は、権限チェック(=認可)よりも先に行うべきです。

なので、ログインチェックフィルターの優先度はPriorities.AUTHENTICATION(=1000)にしています。

(ちなみに、@Priotityアノテーションを付加しない場合、デフォルトではPriorities.USER(=5000)となります)

ExceptionMapperの作成

ログインチェックフィルターと権限チェックフィルターで発生させる、NotAuthorizedExceptionとForbiddenExceptionをキャッチするExceptionMapperを作ります。

それぞれ、401.jspと403.jspにフォワードします。

package com.example.rest.exception.mapper;

import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.glassfish.jersey.server.mvc.Viewable;

@Provider
public class NotAuthorizedExceptionMapper implements ExceptionMapper<NotAuthorizedException> {

    @Override
    public Response toResponse(NotAuthorizedException exception) {
        return Response.status(exception.getResponse().getStatusInfo())
                .entity(new Viewable("/error/401")).build();
    }

}
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>401エラー</title>
    </head>
    <body>
        <p>ログインしていません。</p>
        <a href="${pageContext.request.contextPath}/api/login">ログイン画面へ</a>
    </body>
</html>
package com.example.rest.exception.mapper;

import javax.ws.rs.ForbiddenException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.glassfish.jersey.server.mvc.Viewable;

@Provider
public class ForbiddenExceptionMapper implements ExceptionMapper<ForbiddenException> {

    @Override
    public Response toResponse(ForbiddenException exception) {
        return Response.status(exception.getResponse().getStatusInfo())
                .entity(new Viewable("/error/403")).build();
    }

}
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>403エラー</title>
    </head>
    <body>
        <p>アクセス権がありません。</p>
        <a href="${pageContext.request.contextPath}/api/secret/index">インデックス画面へ</a>
    </body>
</html>

ログアウト用コントローラーの作成

package com.example.rest.resource;

import java.net.URI;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

@Path("logout")
public class LogoutResource {

    @Context
    private HttpServletRequest httpServletRequest;
    @Context
    private UriInfo uriInfo;

    @POST
    public Response logout() throws ServletException {
        // ログアウト
        httpServletRequest.logout();
        // セッション破棄
        HttpSession session = httpServletRequest.getSession();
        if (session != null) {
            session.invalidate();
        }
        // ログイン画面にリダイレクト
        URI loginPage = uriInfo.getBaseUriBuilder()
                .path(LoginResource.class)
                .build();
        return Response.status(Response.Status.SEE_OTHER)
                .location(loginPage)
                .build();
    }
}

実行

管理者権限でログインした場合

f:id:MasatoshiTada:20160130173451p:plain

f:id:MasatoshiTada:20160130173532p:plain

f:id:MasatoshiTada:20160130173558p:plain

普通にアクセス可能です。

管理者以外でログインした場合

f:id:MasatoshiTada:20160130173648p:plain

f:id:MasatoshiTada:20160130173707p:plain

f:id:MasatoshiTada:20160130173726p:plain

管理者専用画面にアクセスすると、「アクセス権がありません。」というエラー画面に遷移します。

未ログイン状態でURLを直接していいた場合

f:id:MasatoshiTada:20160130173934p:plain

f:id:MasatoshiTada:20160130174000p:plain

インデックス画面・管理者専用画面ともに、「ログインしていません。」というエラー画面に遷移します。

まとめ

  • ResourceConfigサブクラスにRolesAllowedDynamicFeatureの登録が必要
  • コントローラーメソッドには@RolesAllowedなどで権限を指定する
  • ログインチェックフィルターには@Priority(Priorities.AUTHENTICATION)を指定する

「Seasar2が終わる!」と慌てるべきではない理由。「Seasar2」という言葉をしっかり定義しよう

2016年9月26日、Seasar2のサポートが終了します。

Seasar2から卒業しよう - DJ HIGAYASUWO (元ひがやすを)

僕自身、大好きなフレームワークです。

EclipseプラグインDoltengなど、開発環境までそろったしっかりそろっているフレームワークって、今でもなかなか無いのではと思います。

仕事の方でも、「Struts 1もSeasar2も開発が終了してしまったので、Java EEやSpringに移行したいと考えています」とご相談いただくことが多いです。

しかし、まずは冷静になる必要があります。

Seasarプロジェクトのすべてが開発終了する訳ではない

Seasar2」という言葉は、DIコンテナ「S2Container」を指すこともあれば、「S2Container + S2JDBC + SAStruts」を指すこともあれば、Seasarプロジェクト全体(http://www.seasar.org/)を指すこともあります。

seasar.orgで紹介されているすべてのプロダクトが開発を終了する訳ではありません。

僕の記憶が確かならば、すでにDomaDBFluteは、コミッターの方が「今後も開発は続ける」(というよりだいぶ前にSeasarから独立している)と宣言されていたはずです。

他にも、そのようなプロダクトは多いのではないでしょうか。

必ず、ご利用しているプロダクトの公式サイトやコミッターの方のブログなどを確認して、今後も開発が続くものと、そうでないものを分けて考えてください。

ひがさんが明言されているのは、

2016/9/26にSeasar2S2JDBCSAStrutsのメンテナンスを現在のコミッタが終了するのは決定で、これは変わりません。 続Seasar2から卒業しよう - DJ HIGAYASUWO (元ひがやすを)

ということだけです。

(この文面における「Seasar2」とは、文脈からおそらく「S2Container」を指すと思われます)

2016/03/25追記

http://www.seasar.orgのトップページに、プロダクトに関する記述が追記されました。

EOLとなるのは以下を除いた全プロダクトです。

DBFlute

DBFlute.NET

Doma

Emecha

Mayaa

S2Container.NET

S2Dao.NET

S2Container + S2JDBC + SAStrutsが使えなくなる訳ではない

これらはオープンソースです。ソースコードGitHubに公開されており、Maven経由で利用も可能です。

2016年9月以降に、どのような形で公開されるかはまだ未定のようですが、公開自体は続くでしょう。

[Seasar-user:22108] Re: Seasar2メンテナンス終了

Mavenリポジトリ、ドキュメント、MLなどがどうなるのかは、現在話し合っている最中です。方向性としては、現在、Seasar2を利用している人々に、最も影響の少ない選択肢が選ばれるはずです。 続Seasar2から卒業しよう - DJ HIGAYASUWO (元ひがやすを)

オープンソースなので、何かあった(バグやセキュリティ脆弱性など)場合、自分で修正することができます。

特にJava SE 8対応関連や、Struts 1.xの脆弱性関連については、常に気を配っていたほうがよいでしょう。

メーリングリストの過去ログ(http://ml.seasar.org/archives/seasar-user/)から、関連しそうなものを下記にまとめましたので、ご確認ください。

Doltengでのプロジェクト作成がJava SE 8では出来ないものの、動作自体の問題は報告されていないようです(Javassistのバージョンを上げげる必要はあるようです)。

Java SE 8対応関連

[Seasar-user:22108] Re: Seasar2メンテナンス終了

[Seasar-user:22057] Re: Java8でのSAStrutsを使用する方法

[Seasar-user:21982] Re: SAStrutsの今後について

[Seasar-user:21953] Re: java8について

Struts 1.x脆弱性関連

[Seasar-user:22048] Re: Validator に入力値検査回避の脆弱性

[Seasar-user:21904] Re: Apache Struts 1脆弱性について

[Seasar-user:21902] StrutsのClassLoader脆弱性はSAStrutsに影響しません

[Seasar-user:21909] Re: JavaBeansに対するリフレクションとClassLoader脆弱

Java SE 9以降への対応

2017年にJava SE 9がリリースされます。

これが何を意味しているかというと、その1年後の2018年に、Oracle社によるJava SE 8の無償サポートが終了する可能性が高いことを意味します。

(2014年にJava SE 8がリリースされた時も、その1年後の2015年に、Java SE 7の無償サポートが終了しました)

これも基本的には、ご自分でメンテナンスする必要があります。

S2Container + S2JDBC + SAStrutsを使わないほうがよいケース

一言で言うと、自分でメンテナンスする工数が割けない場合です。

この場合は、素直にSpringなりJava EEなりに移行したほうが良いでしょう。

メンテナンスができない場合は、少なくとも新規開発でS2JDBC + SAStrutsの採用はやめた方がよいでしょう。

SAStruts + S2JDBCから移行するなら?

僕が知っている範囲では、単純に機能面だけ見れば、SAStruts + S2JDBCに近いのは「Spring MVC + Doma」だと思っています。

もしくは、Domaの代わりに@cero_tさんのBootiful SQL Templateを使うか。

Bootiful SQL Templateという名前にしてMavenリポジトリで公開しました。 - 谷本 心 in せろ部屋

Java EEは、SAStruts + S2JDBCとの機能的な近さだけで言えば、Springには劣ります。

Java EEは、アプリケーションサーバーのサポートや長期的な仕様の安定性が魅力なので、これらのメリットを享受したいならばJava EEもアリかと思います。

その場合、研修は私にお任せくd(ry

まとめ

  • 開発が終了するプロジェクトと、そうでないプロジェクトをしっかりと区別する必要があります。「Seasar2が終わってしまう」とひとくくりにして慌てる必要はありません。
  • S2Container、SAStrutsS2JDBCを使い続ける場合、ご自分でメンテナンスをする必要があります。
  • メンテナンスの工数が割けない場合は、S2Container、SAStrutsS2JDBCの利用はやめた方がいいです。

以上です。

最後に、ありがとう、Seasar2。今も大好きです。

DBから取ってきたエンティティをビューやRESTに直接渡すべきではない理由?

とりあえず、見つけた記事を自分用にメモ。

極北データモデリング

パトリオットミサイル: プログラマーの雑記帳

知りたいこと

  • なぜエンティティをDTOに詰め替えすべきなのか?
  • 詰め替えはどのクラスでやるべきなのか?コントローラー?ビジネスロジック?DAO?その他?

研修ばかりやってると、エンティティを直接ビューで表示するようなプログラムばかりなので・・・