MVC1.0とは? †
- 昔々のWebアプリは、データのやり取りをするごとに画面を全部書き換えていた
- でも、今日日のWebアプリは、クライアントMVC
- 昔々々のクラサバシステムに先祖返りして、ブラウザ側では Javascript アプリが動く
- Javascript アプリは適宜 REST でサーバに問い合わせを行ってデータの出し入れをする
- めったに画面の書き換えは置きない (1page app)
- で、いくら 1page app といっても、最初にブラウザに HTML を送る必要がある。そのとき、サーバ側の情報をなんか埋め込みたいよね → MVC1.0
- REST (JAX-RS) を拡張して、XML や JSON の他に HTML を返せるようにした
- HTML を作るときにテンプレートエンジンを使って、テンプレートとサーバ側の情報を組み合わせることができる
- JavaEE8 (2018) で MVC1.0 を制式採用予定。実装には Ozark が採用されるらしい
- ここでは、
最初に普通の JavaEE7 Web アプリを作る †
MVC1.0 関連のライブラリ (Ozark, Thymeleaf) を追加 †
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>MVCExam</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>MVCExam</name>
<properties>
<endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish.ozark</groupId>
<artifactId>ozark</artifactId>
<version>1.0.0-m02</version>
</dependency>
<!--
This ext library depends on the thymeleaf 2, not 3.
In the thymeleaf 2, the template must be XHTML. But in the thymeleaf 3,
the template is allowed free style HTML. So, I write ozark extension
for thymeleaf3, currently.
<dependency>
<groupId>org.glassfish.ozark.ext</groupId>
<artifactId>ozark-thymeleaf</artifactId>
<version>1.0.0-m02</version>
</dependency>
-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.6.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${endorsed.dir}</outputDirectory>
<silent>true</silent>
<artifactItems>
<artifactItem>
<groupId>javax</groupId>
<artifactId>javaee-endorsed-api</artifactId>
<version>7.0</version>
<type>jar</type>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- その他に今日日 lombok は絶対欲しいでずね。getter/setter と logger の自動生成
Ozark のテンプレートエンジンとして Thymeleaf3 を使う †
- まず Ozark は、CDI に全面的に依存しているので /WEB-INF/beans.xml を作る
<?xml version="1.0" encoding="UTF-8"?>
<beans 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/beans_1_1.xsd"
bean-discovery-mode="annotated">
</beans>
- bean-descovery-mode は、Netbeans が作ってくれる annotated で OK
bean-descovery-mode | |
all | すべてのクラスに、暗黙的に @Dependent (Injectする側のスコープを継承) がついていると見なす |
annotated | 明示的に CDI の Annotation がついているクラス/メソッドを CDI と見なす |
none | すべてのクラスを CDI の対象外にする |
- MVC 1.0 の Template Engine は、CDI から参照できる View Engine が使われる
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.ext.ozark;
import java.io.IOException;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.mvc.engine.ViewEngineContext;
import javax.mvc.engine.ViewEngineException;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.ozark.engine.ViewEngineBase;
import org.glassfish.ozark.engine.ViewEngineConfig;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
/**
* Execute Template Engine.
* @author hondou.atsushi
*/
@Slf4j
@ApplicationScoped
public class ThymeleafViewEngine extends ViewEngineBase {
@Inject
private ServletContext servletContext;
@Inject
@ViewEngineConfig
private TemplateEngine engine;
@Override
public boolean supports(String view) {
return view.endsWith(".html");
}
@Override
public void processView(ViewEngineContext context) throws ViewEngineException {
try {
HttpServletRequest request = context.getRequest();
HttpServletResponse response = context.getResponse();
WebContext ctx = new WebContext(request, response, servletContext, request.getLocale());
ctx.setVariables(context.getModels());
// Don't add prefix to the URL of the template.
// For more details, see DefaultTemplateEngineProducer.java
engine.process(context.getView() /* resolveView(context) */
, ctx, response.getWriter());
} catch (IOException e) {
throw new ViewEngineException(e);
}
}
}
- supports() で、テンプレートのファイル名から、このテンプレートエンジンを適用するかを返す
- processView() で、テンプレートを使って HTTP Response をおこなう
- テンプレートエンジンを動かすごとに毎回初期設定からやるのは大変なので、設定済みの View Engine を Inject する → CDI の Producer 機能を使って DefaultTemplateEngineProducer?.java で View Engine のインスタンスを作る
- View Engine の初期設定は Template Engine Producer で行う
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.ext.ozark;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.mvc.engine.ViewEngine;
import javax.servlet.ServletContext;
import org.glassfish.ozark.engine.ViewEngineConfig;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
/**
* Bind Thymeleaf3 to MVC1.0.
* @author hondou.atsushi
*/
@Dependent
public class DefaultTemplateEngineProducer {
@Produces
@ViewEngineConfig
public TemplateEngine getTemplateEngine(ServletContext context) {
ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(context);
// Ozark 1.0.0m2 add "WEB-INF/views" to prefix at resolveView(context) on ViewEngine.
// And resolver.prefix is "" as defult value.
// It is convenient to write a controller of MVC1.0.
// For example, if the return value of the MVC1.0 controller is "hello.html", that
// means the Ozark use "/WEB-INF/view/hello.html" as template.
//
// But it is not convenient to specify fragment resources on a html template.
// In the default settings, we must write like followings:
// <div th:replace="/WEB-INF/views/common/layout.html :: header">HEADER</div>.
// It's not good.
//
// So, I defined resolver.prefix "/WEB-INF/views" and NOT add prefix on the ViewEngine.
// By doing so, we can omit the prefix of the file path either in the controller or
// the template.
resolver.setPrefix(ViewEngine.DEFAULT_VIEW_FOLDER);
// Default template mode is "html".
//resolver.setTemplateMode(TemplateMode.HTML);
// Default charset is null that represents to depend on environment.
resolver.setCharacterEncoding("UTF-8");
TemplateEngine engine = new TemplateEngine();
engine.setTemplateResolver(resolver);
engine.setMessageResolver(new MessageResolver());
return engine;
}
}
Thymeleafの機能を使って、i18n †
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.ext.ozark;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;
import lombok.extern.slf4j.Slf4j;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.messageresolver.AbstractMessageResolver;
import org.thymeleaf.util.Validate;
/**
* Process placeholders on Thymeleaf3 templates.
* @author hondou.atsushi
*/
@Slf4j
public class MessageResolver extends AbstractMessageResolver {
@Override
public String resolveMessage(ITemplateContext context, Class<?> origin, String key, Object[] messageParameters) {
ResourceBundle.Control control =
ResourceBundle.Control.getNoFallbackControl(
ResourceBundle.Control.FORMAT_DEFAULT);
Locale locale = context.getLocale();
log.debug("Message Locale={}", locale.toString());
ResourceBundle bundle = ResourceBundle.getBundle("msg", locale, control);
String message = bundle.getString(key);
if (null == messageParameters || 0 == messageParameters.length) {
return message;
}
final MessageFormat messageFormat = new MessageFormat(message, locale);
return messageFormat.format(messageParameters);
}
@Override
public String createAbsentMessageRepresentation(ITemplateContext context, Class<?> origin, String key, Object[] messageParameters) {
Validate.notNull(key, "Message key cannot be null");
if (context.getLocale() != null) {
return "??"+key+"_" + context.getLocale().toString() + "??";
}
return "??"+key+"_" + "??";
}
}
- このクラスを DefaultTemplateEngineProducer?.java で、Thymeleaf に設定している
- classpath 上にある properties ファイルを使う
- 言語は、HTTP Request から取得
- まぁ、この応用で外部のファイルからとってもいいし、データベースからとっても良い
- テンプレートからの参照方法は Hello MVC1.0 で
CDIの機能を使って前処理・後処理 †
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.ext.ozark;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.mvc.event.AfterControllerEvent;
import javax.mvc.event.AfterProcessViewEvent;
import javax.mvc.event.BeforeControllerEvent;
import javax.mvc.event.BeforeProcessViewEvent;
import javax.mvc.event.ControllerRedirectEvent;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
/**
* MVC1.0の共通処理サンプル.
* <pre>
* コントローラ、Viewエンジンの開始終了に何らかの処理をはさむことができます。
* ただし Model を操作することはできません。
* </pre>
* @author hondou.atsushi
*/
@Slf4j
@ApplicationScoped
public class CommonEventObservver {
@Inject
private HttpServletRequest req;
public void onAfterControllerEvent(@Observes AfterControllerEvent event) {
log.debug("AfterControllerEvent");
req.setAttribute("__USER_NAME", req.getRemoteUser());
try {
req.setAttribute("__HOST_NAME", InetAddress.getLocalHost().getHostName());
} catch (UnknownHostException e) {
req.setAttribute("__HOST_NAME", "###");
}
}
public void onAfterProcessViewEvent(@Observes AfterProcessViewEvent event) {
log.debug("AfterProcessViewEvent");
}
public void onBeforeControllerEvent(@Observes BeforeControllerEvent event) {
log.debug("BeforeControllerEvent");
}
public void onBeforeProcessViewEvent(@Observes BeforeProcessViewEvent event) {
log.debug("BeforeProcessViewEvent");
}
public void onControllerRedirectEvent(@Observes ControllerRedirectEvent event) {
log.debug("ControllerRedirectEvent");
}
}
- MVC1.0は、ライフサイクル中で CDI イベントを発行している
- MVC1.0処理に関して、アプリ横断で何か前処理・後処理をしたければ、それらのイベントを拾えばいい
- この例では、HTTP Request からホスト名・ユーザ名を取り出して Thymeleaf から参照できるるようにする
Hello MVC1.0 †
- JAX-RS部分
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.rest;
import javax.enterprise.context.RequestScoped;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Hello Bean.
* @author hondou.atsushi
*/
@RequestScoped
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HelloBean {
private String name;
}
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.rest;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.mvc.Models;
import javax.mvc.annotation.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
/**
* REST Web Service
*
* @author hondou.atsushi
*/
@Controller
@Path("/hello")
@RequestScoped
@Slf4j
public class HelloController {
@Context
private UriInfo context;
@Inject
private HelloBean helloBean;
@Inject
private Models models;
/**
* Creates a new instance of ViewResource
*/
public HelloController() {
}
/**
* Retrieves representation of an instance of com.example.mvcexam.rest.ViewResource
* @return an instance of java.lang.String
*/
@GET
public String helloMVC() {
log.info("MVC1.0 was called!");
helloBean.setName("Cathy");
// MVC1.0+Thymeleaf3 の規格上は、テンプレートから CDI を参照できるはずだけど
// 現時点で models に格納した bean しか参照できない
// それはそれで (コードの統制上) いいかもしれない。
models.put("helloBean", helloBean);
return "hello.html";
}
}
- 普通の JAX-RS クラスに @Controller をつけると MVC1.0 になる
- HTTP Response で返す HTML の指定の仕方
- 返り値にテンプレート名を指定する return "hello.html" → /WEB-INF/view/hello.html (prefix の /WEB-INF/view は、DefaultTemplateEngineProducer? で Thymeleaf にそう設定したから)
- テンプレートに埋め込むデータは @Inject した Model に設定する
- i18n
hello=Hello!
hello=\u3053\u3093\u306b\u3061\u308f
hello=\u4f60\u597d
- さっき作った MessageResolver?.java で、HTTP Request の言語設定 (ブラウザのデフォルト言語) にあうものが使われる
- Template (hello.html)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello Tymeleaf3</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div th:replace="common/layout.html :: header">HEADER</div>
<h1>Hello Tymeleaf3 + MVC1.0</h1>
<div><span th:text="#{hello}">HELLO</span> [[${helloBean.name}]]</div>
<div th:replace="common/layout.html :: footer">FOOTER</div>
</body>
</html>
- 基本的な使い方が盛り沢山
- IDE(Netbeans/Eclipse)のHTMLエディタで、th:xxx タグを認識させるために
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
- フラグメント(共通ヘッダ・フッタ)の呼び出し
<div th:replace="common/layout.html :: header">HEADER</div>
<div th:replace="common/layout.html :: footer">FOOTER</div>
- i18n由来のデータの表示
<span th:text="#{hello}">HELLO</span>
HELLO が #{hello} で置き換えられる
- Model由来のデータの表示
[[${helloBean.name}]]
これは、タグがない場合の書き方
<span th:text="${helloBean.name}">NAME</span>
でもOK
- 重要 : Thymeleaf では、テンプレート上の要素を th:xxx で 上書きする 。つまり、テンプレートエンジンを通さなくても、仮の値でそのままブラウザに表示できる。テンプレートHTMLを変更して、ある程度はそのままブラウザで表示確認できることは、HTML 作成の生産性に大きく寄与する。
- 共通ヘッダ・共通フッタ
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div th:fragment="header">
<div style="background-color: cyan; width:100%">
共通ヘッダー
</div>
</div>
<div th:fragment="footer">
<div style="color:white; background-color: brown; width:100%">
共通フッター
</div>
</div>
</body>
</html>
- 動作イメージ
MVC1.0 でも JAX-RS を使った Bean Validation †
- Controller
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.rest;
import javax.validation.constraints.Size;
import javax.ws.rs.FormParam;
import lombok.Data;
/**
* Validation Bean.
* @author hondou.atsushi
*/
@Data
public class ValidationBean {
@FormParam("name")
@Size(min = 1, max = 255)
private String name;
@FormParam("comment")
@Size(min = 1, max = 255)
private String comment;
}
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.rest;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.mvc.Models;
import javax.mvc.annotation.Controller;
import javax.mvc.annotation.CsrfValid;
import javax.mvc.binding.BindingResult;
import javax.validation.Valid;
import javax.validation.executable.ExecutableType;
import javax.validation.executable.ValidateOnExecution;
import javax.ws.rs.BeanParam;
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.UriInfo;
import lombok.extern.slf4j.Slf4j;
/**
* REST Web Service
* @author hondou.atsushi
*/
@Controller
@Path("/validation")
@RequestScoped
@Slf4j
public class ValidationController {
@Context
private UriInfo context;
@Inject
private Models models;
@Inject
private BindingResult bindingResult;
/**
* Creates a new instance of ViewResource
*/
public ValidationController() {
}
@Path("/init")
@GET
public String validateForm() {
return "commentform.html";
}
/**
* Retrieves representation of an instance of com.example.mvcexam.rest.ViewResource
* @param bean
* @return an instance of java.lang.String
*/
@Path("/submit")
@POST
@CsrfValid
@ValidateOnExecution(type=ExecutableType.NONE)
public String validateForm(@Valid @BeanParam ValidationBean bean) {
log.info("validateForm was called!");
if (bindingResult.isFailed()) {
models.put("form", bean);
models.put("bindingResult", bindingResult);
return "fail.html";
}
models.put("form", bean);
return "success.html";
}
}
- 通常の Bean Validation と同様に @Valid で検証を行う。検証内容は Bean のアノテーションに記載する
- @ValidateOnExecution? で、Bean Validation 失敗時に処理を継続することを宣言する。
- Bean Validation の結果は、Injection されている ValidationResult? に格納される
- 遷移元 HTML (Thymeleaf)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello Tymeleaf3</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div th:replace="common/layout.html :: header">HEADER</div>
<h1>Comment Form</h1>
<form th:action="@{/webresources/validation/submit}" method="POST">
NAME: <input type="text" name="name"/> <br/>
COMMENT: <br/>
<textarea name="comment"></textarea> <br/>
<input type="submit"/>
<input type="hidden" th:name="${mvc.csrf.name}" th:value="${mvc.csrf.token}"/>
</form>
<div th:replace="common/layout.html :: footer">FOOTER</div>
</body>
</html>
- 遷移先 HTML (Thymeleaf)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello Tymeleaf3</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div th:replace="common/layout.html :: header">HEADER</div>
<h1>[[${form.name}]], thank you for the comment.</h1>
<div th:text="${form.comment}">HELLO</div>
<div th:replace="common/layout.html :: footer">FOOTER</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello Tymeleaf3</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div th:replace="common/layout.html :: header">HEADER</div>
<h1>There are some input errors.</h1>
<div style="color:red">
<ul th:each="err : ${bindingResult.allViolations}">
<li>[[${err}]]</li>
</ul>
</div>
<form th:action="@{/webresources/validation/submit}" method="POST">
NAME: <input type="text" name="name" th:text="${form.name}"/> <br/>
COMMENT: <br/>
<textarea name="comment">[[${form.comment}]]</textarea> <br/>
<input type="submit"/>
<input type="hidden" th:name="${mvc.csrf.name}" th:value="${mvc.csrf.token}"/>
</form>
<div th:replace="common/layout.html :: footer">FOOTER</div>
</body>
</html>
- 動作イメージ
MVC1.0 の機能を使って CSRF 対策 †
- CSRF cross-site request forgeries : 別のサイトからの HTTP REQUEST を受け取って処理しちゃうこと
- 基本的な対策としては、入力フォーム生成時に乱数のキーを発行して、フォーム提出時に照合する
- さっきの Bean Validation のサンプルに CSRF 対策が入っている
- MVC1.0 で HTML を作るときには、暗黙オブジェクト ${mvc.csrf.name} ${mvc.csrf.token} が作られるので、これをテンプレート(Thymeleaf)で入力フォームのhidden項目に埋め込む
<form th:action="@{/webresources/validation/submit}" method="POST">
NAME: <input type="text" name="name"/> <br/>
COMMENT: <br/>
<textarea name="comment"></textarea> <br/>
<input type="submit"/>
<input type="hidden" th:name="${mvc.csrf.name}" th:value="${mvc.csrf.token}"/>
</form>
- JAX-RS 側には @CsrfValid? をつけるだけ
@Path("/submit")
@POST
@CsrfValid
@ValidateOnExecution(type=ExecutableType.NONE)
public String validateForm(@Valid @BeanParam ValidationBean bean) {
log.info("validateForm was called!");
if (bindingResult.isFailed()) {
models.put("form", bean);
models.put("bindingResult", bindingResult);
return "fail.html";
}
models.put("form", bean);
return "success.html";
}
- 実行イメージ。CSRF 対策の token が無い Form から /submit を呼ぶと 500 Internal Server Error になる
- CSRF違反を個別に処理したい → Glassfish4 (のJAX-RS実装のJersey) では無理みたい
- 規格上は JAX-RS の ExceptionMapper? を作れば良いはずだけど、規格上 ExceptionMapper? の適用順を制御できないので JAS-RS デフォルトの ExceptionMapper? が最初に適用されてしまう。結局、↓は CSRF 時に適用されないけど、まぁ不正ユーザからの HTTP REQUEST を弾ければいいので、500 Error でいいかな。もちろん Glassfish デフォルトのエラー画面の差し替えはいるけど
/*
* An example for the MVC1.0m2 on the Glassfish 4.1.2 JEE7
*/
package com.example.mvcexam.rest;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.mvc.Models;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
/**
* CsrfExceptionMapper.
* Don't forget ApplicationScoped.
* If the annotation is not, Glassfish says
* "There was no object available for injection at SystemInjecteeImpl"
* beceuse this class is not managed by CDI.
* @author hondou.atsushi
*/
@Provider
@ApplicationScoped
public class CsrfExceptionMapper implements ExceptionMapper<ForbiddenException> {
@Inject
private Models models;
@Override
public Response toResponse(ForbiddenException exception) {
models.put("message", exception.getMessage());
return Response
.status(Response.Status.BAD_REQUEST)
.entity("error.html")
.build();
}
}
Thymeleaf3 を使うもう一つの利点 †
参考文献 †
Java#Glassfish