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を使うためのライブラリを作ってみました。

ソースコードGitHubにあります。

  • 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サイトに公開されています。

Extending Thymeleaf

条件によって表示/非表示を切り替える属性を作る

Apache Shiroの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>タグです。これは、ログイン中のユーザー名を表示するタグです。

<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に格納して返します。

IntelliJEclipseで属性の補完ができるようにする

この作業は必須ではないのですが、やっておくと便利です。

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を利用するときに、IntelliJEclipse(Thymeleafプラグイン必須)で属性の補完が効くようになります。

f:id:MasatoshiTada:20161219105028p:plain

作成した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を追加する

参考資料

Apache Shiroのミニブック

Apache Shiroカスタムタグのソースコード

thymeleaf-extras-springsecurityのソースコード