ThymeleafでApache Shiroを使うためのライブラリを作ってみた
この記事は?
Java EE Advent Calendar 2016の19日目です。
昨日の記事は@khasunumaさんの「MicroProfile によるアプリケーション開発」でした。明日は@emaggameさんです。
今回は、以前@n_agetsuさんが紹介されていたApache Shiroというセキュリティフレームワークを、Thymeleafで使ってみました。
Apache Shiro を使ってみました - 見習いプログラミング日記
上妻さんのGitHubのサンプルをforkして、ビューをJSPからThymeleafに書き換えました。
合わせて、ThymeleafでApache Shiroを使うためのライブラリを作ってみました。
- ThymeleafのApache Shiro用Dialect
GitHub - MasatoshiTada/thymeleaf-extras-shiro
- 上妻さんのサンプルをThymeleafに移植したもの
GitHub - MasatoshiTada/ShiroSample: Apache Shiro 1.2 JDBC Realm sample
Dialectとは
上妻さんのブログでも紹介されている通り、Apache ShiroにはJSPカスタムタグが用意されています(タグの一覧はこちら)。
今回は、これらのJSPカスタムタグと似たようなものを、Thymeleafで作成しました。
Thymeleafといえば、タグの中にth:
で始まる属性を記述していくのが特徴です。これらの属性は、自作することも可能です。
作成できるものは、属性の他、タグやユーティリティオブジェクト*1などです。
Thymeleafでは、これらを1つにまとめて「Dialect」と呼ばれます。
Dialectを自作する方法は、Thymeleaf公式Webサイトに公開されています。
条件によって表示/非表示を切り替える属性を作る
Apache ShiroのJSPカスタムタグは、認証済み/認証済みでない等の条件によって、タグで挟んだ部分を表示する/しないを切り替えるものがほとんどです。
- JSPの場合
<%-- このタグで挟まれた部分はログイン中のみ表示される --%> <shiro:authenticated> <h3>Hello, <shiro:principal/></h3> <form action="/ShiroSample/users/logout" method="post"> <div class="form-group"> <button type="submit" id="logout-button" class="btn btn-primary">logout</button> </div> </form> </shiro:authenticated>
この<shiro:authenticated>
タグを、Thymeleafの属性で下記のように作り変えてみました。(<shiro:principal>
タグについては後述します)
- Thymeleafの場合
<!--/* この属性で挟まれた部分はログイン中のみ表示される */--> <div shiro:authenticated> <h3>Hello, <span shiro:principal>foo@sample.com</span></h3> <form th:action="@{/users/logout}" action="./index.html" method="post"> <div class="form-group"> <button type="submit" id="logout-button" class="btn btn-primary">logout</button> </div> </form> </div>
shiro:authenticated
属性は、今回僕が作成したものです。機能はJSPカスタムタグと同じです。
条件によって表示する/しないを切り替える属性は、ThymeleafのAbstractStandardConditionalVisibilityTagProcessor
クラスを継承すれば、簡単に作成できます。
このクラスにisVisible()
という抽象メソッドが用意されているので、これをオーバーライドするだけです。
- 全属性の親クラス
public abstract class AbstractSecureAttributeProcessor extends AbstractStandardConditionalVisibilityTagProcessor { public static final int ATTR_PRECEDENCE = 300; public AbstractSecureAttributeProcessor(String dialectPrefix, String attrName) { super(TemplateMode.HTML, dialectPrefix, attrName, ATTR_PRECEDENCE); } protected final Subject getSubject() { return SecurityUtils.getSubject(); } protected abstract boolean isVisible(ITemplateContext context, IProcessableElementTag tag, AttributeName attributeName, String attributeValue); }
- 認証済みの時のみ表示する属性
public class AuthenticatedAttributeProcessor extends AbstractSecureAttributeProcessor { /** HTMLで指定する属性名 */ private static final String ATTR_NAME = "authenticated"; public AuthenticatedAttributeProcessor(String dialectPrefix) { super(dialectPrefix, ATTR_NAME); } /** * 認証済みであればtrue */ @Override protected boolean isVisible(ITemplateContext context, IProcessableElementTag tag, AttributeName attributeName, String attributeValue) { return getSubject() != null && getSubject().isAuthenticated(); } }
親クラスを作っているのは、ShiroのJSPカスタムタグのソースコードを参考にしました。
AbstractStandardConditionalVisibilityTagProcessor
クラスは、前述のドキュメントには書かれていないのですが、Spring Security用のThymeleaf拡張のソースコードを読んで知りました。
他にも、ロールの有無やパーミッションの有無で表示/非表示を切り替える属性を作ったのですが、すべて同じ作り方です。詳細はGitHubをご覧ください。
値を表示する属性を作る
さて、ShiroのJSPカスタムタグの中で、1つだけ役割が違うのが<shiro:principal>
タグです。これは、ログイン中のユーザー名を表示するタグです。
- JSPの場合
<h3>Hello, <shiro:principal/></h3>
- Thymeleafの場合
<h3>Hello, <span shiro:principal>foo@sample.com</span></h3>
値を表示する属性は、 ThymeleafのAbstractAttributeTagProcessor
クラスを継承し、doProcess()
メソッドをオーバーライドして作成します。
public class PrincipalAttributeProcessor extends AbstractAttributeTagProcessor { private static final String ATTR_NAME = "principal"; public PrincipalAttributeProcessor(String dialectPrefix) { super(TemplateMode.HTML, dialectPrefix, null, false, ATTR_NAME, true, 1000, true); } /** * 認証済みであればtrue */ protected boolean isAuthenticated() { return getSubject() != null && getSubject().isAuthenticated(); } @Override protected void doProcess(ITemplateContext context, IProcessableElementTag tag, AttributeName attributeName, String attributeValue, IElementTagStructureHandler structureHandler) { String name = isAuthenticated() ? getSubject().getPrincipal().toString() // ログイン済みならばユーザー名 : "GUEST!!!"; // ログイン済みでなければ"GUEST!!!"を代替のユーザー名とする // 値を出力する structureHandler.setBody(HtmlEscape.escapeHtml5(name), false); } }
ちょっと長いですが、値を出力しているのは、最後の行のstructureHandler.setBody()
です。
作った属性をまとめて、1つのDialectを定義する
public class ShiroDialect extends AbstractProcessorDialect { private static final String DIALECT_NAME = "Shiro Dialect"; public ShiroDialect() { super(DIALECT_NAME, "shiro", StandardDialect.PROCESSOR_PRECEDENCE); } @Override public Set<IProcessor> getProcessors(String dialectPrefix) { final Set<IProcessor> processors = new HashSet<>(); processors.add(new GuestAttributeProcessor(dialectPrefix)); processors.add(new NotAuthenticatedAttributeProcessor(dialectPrefix)); processors.add(new UserAttributeProcessor(dialectPrefix)); processors.add(new AuthenticatedAttributeProcessor(dialectPrefix)); processors.add(new HasRoleAttributeProcessor(dialectPrefix)); processors.add(new LacksRoleAttributeProcessor(dialectPrefix)); processors.add(new PrincipalAttributeProcessor(dialectPrefix)); processors.add(new HasAnyRoleAttributeProcessor(dialectPrefix)); processors.add(new HasPermissionAttributeProcessor(dialectPrefix)); processors.add(new LacksPermissionAttributeProcessor(dialectPrefix)); return processors; } }
AbstractProcessorDialect
クラスを継承して、getProcessors()
メソッドをオーバーライドします。
このメソッドの中で、作成した属性を表すクラスをインスタンス化して、すべてSet
に格納して返します。
IntelliJやEclipseで属性の補完ができるようにする
この作業は必須ではないのですが、やっておくと便利です。
src/main/resources配下*2に、下記のようなXMLを作成します。
<?xml version="1.0" encoding="UTF-8"?> <dialect xmlns="http://www.thymeleaf.org/extras/dialect" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.thymeleaf.org/extras/dialect http://www.thymeleaf.org/xsd/thymeleaf-extras-dialect-2.1.xsd" prefix="shiro" namespace-uri="http://suke_masa.com/thymeleaf/shiro" namespace-strict="false" class="com.suke_masa.thymeleaf.extras.shiro.dialect.ShiroDialect"> <attribute-processor name="authentication" class="com.suke_masa.thymeleaf.extras.shiro.dialect.processor.AuthenticatedAttributeProcessor"> <documentation reference="authentication attribute"/> </attribute-processor> <attribute-processor name="principal" class="com.suke_masa.thymeleaf.extras.shiro.dialect.processor.PrincipalAttributeProcessor"> <documentation reference="principal attribute"/> </attribute-processor> <!-- 一部省略 --> </dialect>
これを作っておくと、このDialectを利用するときに、IntelliJやEclipse(Thymeleafプラグイン必須)で属性の補完が効くようになります。
作成したDialectを使う
TemplateEngine
クラスのaddDialect()
メソッドで、作成したDialectを追加します。
@Provider public class ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> { @Context private HttpServletRequest httpServletRequest; @Context private HttpServletResponse httpServletResponse; private TemplateEngine templateEngine; @Inject public ThymeleafTemplateProcessor(Configuration config, ServletContext servletContext) { super(config, servletContext, "html", "html"); ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext); templateResolver.setPrefix((String) config.getProperty(MvcFeature.TEMPLATE_BASE_PATH)); templateResolver.setTemplateMode(TemplateMode.HTML); templateEngine = new TemplateEngine(); // 作成したDialectを追加する templateEngine.addDialect(new ShiroDialect()); templateEngine.setTemplateResolver(templateResolver); } // 以下省略
まとめ
AbstractStandardConditionalVisibilityTagProcessor
を継承して、条件で表示/非表示を切り替える属性を作るAbstractAttributeTagProcessor
を継承して、値を出力する属性を作るAbstractProcessorDialect
を継承してDialectを作成し、作った属性を1つにまとめる- XMLを作成して、IDEで補完が効くようにする
TemplateEngine
クラスのaddDialect()
メソッドで、作成したDialectを追加する