本格派エンジニアの工具箱

第22回Apache Shiroを利用してWebアプリケーションに認証機構を組み込む

Webアプリケーションへのインテグレーション

前々回に引き続いて、Javaアプリケーション向けの認証フレームワークApache Shiro⁠以下Shiro)の使い方を解説します。ShiroはServletによるWebアプリケーションに組み込んで利用することもできます。Shiroを利用することでWebサイトへのログイン管理が簡単になる他、ページごとに認証方法を変えるなどといった細かな設定も容易に行えるようになります。

ShiroとWebアプリとの連携はServletフィルタの機構を利用して行います。Shiroにはjavax.servlet.Filterインターフェースを実装した基底クラスorg.apache.shiro.web.servlet.AbstractFilterと、これを継承したさまざまなクラスが用意されています。Webアプリ側では、クライアントからのリクエストがこれらのフィルタを経由するようweb.xmlに設定を追加するだけで、Shiroによる認証機構を利用できるようになります。

web.xmlの設定は次のように記述します(ただし、次期バージョンのShiro 1.2からは記述方法が少し変わります。詳細はリファレンスマニュアルを参照してください⁠⁠。IniShiroFilterはShiroの設定をINIファイルから読み込むフィルタです。この例では、設定ファイルshiro.iniを/WEB-INF/ディレクトリに配置し、それを読み込むようになっています。

<filter>
  <filter-name>ShiroFilter</filter-name>
  <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
  <init-param>
    <param-name>configPath</param-name>
    <param-value>/WEB-INF/shiro.ini</param-value> 
  </init-param>
</filter>

その他に、Shiroの設定を直接web.xmlに記述してしまう方法もあります。その場合はinit-paramのparam-nameを「config」として、設定内容をparam-valueの値として直接書き込みます。

<filter>
  <filter-name>ShiroFilter</filter-name>
  <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
  <init-param>
    <param-name>config</param-name>
    <param-value>
      [main]
      ……
      [urls]
      /index.jsp = anon
      ……
      [users]
      gihyo = gihyopass
    </param-value>
  </init-param>
</filter>

INIファイルの書き方は、基本的にはスタンドアロンの場合と同様です。ただし、Webアプリで利用する場合にはここにページごとに使用する認証フィルタの設定を記述しておく必要があります。ページごとの認証フィルタの設定は[urls]セクションに記述します。形式は「⁠⁠コンテキストルートパス】=【フィルタの種類⁠⁠」です。パスにはワイルドカードとして「**」を使うこともできます。たとえば「/user/**」と記述した場合には、/user/ディレクトリ以下のすべてのコンテンツへのアクセスが対象となります。

認証フィルタには下表のようなものがあります。その他に、Shiro 1.2からはログアウト処理を行う「logout」やセッションを作成しない「noSessionCreation」などが利用できるようになる予定です。複数の認証フィルタを適用することも可能で、その場合にはINIファイルの設定でフィルタ名をコンマで区切って記述します。

 認証フィルタの種類
フィルタ名内容使用されるクラス名(パッケージ名は省略)
anon認証を行わない。AnonymousFilter
authcフォームを使ったログイン認証を行う。認証されていないアクセスはログインページへリダイレクトするFormAuthenticationFilter
authcBasicHTTPのBasic認証を行う。BasicHttpAuthenticationFilter
permsログイン中のユーザからのコンテンツごとのアクセス権を制御するPermissionsAuthorizationFilter
port特定のポートへのリクエストのみ許可する。PortFilter
restHTTPリクエスト別にアクセス制御を行う。RESTサービスなどの実装に利用するHttpMethodPermissionFilter
rolesロール別のアクセス権を制御するRolesAuthorizationFilter
sslSSLによる認証を行うSslFilter
user既知のユーザ(認証済みか、またはRememberMe機構によって記憶されたユーザ)からのアクセスのみ許可するUserFilter

認証フィルタもServletフィルタの一種です。たとえば「authc」を指定されているページへのアクセスではFormAuthenticationFilterが使われます。つまりクライアントからのアクセスは最初にweb.xmlに設定されたフィルタを通じてアクセスするページごとの認証フィルタに振り分けられることで、Shiroの認証機構の管理下に置かれるというわけです。

以下に、設定ファイルshiro.iniの記述例を示します。この例では、コンテキストルートのindex.jspとlogin.jspは「anon」が指定しされているので認証なしでアクセスできますが、userディレクトリ以下のコンテンツにアクセスした場合には、⁠authc」フィルタによる認証が必要となります。authcは認証済みのユーザ以外からのアクセスのみ受け付け、それ以外はログインページにリダイレクトします。ログインページは[filters]セクションのauthc.loginUrlプロパティに設定します。[users]セクションにはこれまで同様、ユーザ名とパスワードを記述してあります。

;[main]
;securityManager.sessionManager.sessionValidationSchedulerEnabled = false

;sessionListener = jp.gihyo.toolbox.shiro.MySessionListener
;securityManager.sessionManager.sessionListeners = $sessionListener
[filters]
authc.loginUrl = /login.jsp

[urls]
/index.jsp = anon
/login.jsp = anon
/user/** = authc

[users]
gihyo = gihyopass

ログイン処理を行うプログラムの作成

続いてログインページやServletのプログラムを作っていきましょう。まず、ログインページとなる/login.jspでは、次のようにユーザ名とパスワードを入力できるようにします。

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Shiroサンプル - ログインページ</title>
  </head>
  <body>
    <h1>ログインページ</h1>
    
    <form action="Login" method="POST">
      ユーザ名: <input type="text" name="username"/> <br/>
      パスワード: <input type="password" name="password"/> <br/>
      <input type="submit" name="loginButton" value="ログイン"/>
    </form>
    
  </body>
</html>

ログインページからの入力を処理するServletはLogin.javaとして次のように作りました。

public class Login extends HttpServlet {
  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
          throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");

    String url = "/login.jsp";    // リダイレクト先のURL
    
    // ユーザ名とパスワードを取得
    String username = request.getParameter("username");
    String password = request.getParameter("password");

    // Tokenを作成
    UsernamePasswordToken token = 
        new UsernamePasswordToken(username, password);
    
    try {
      // Subjectを取得
      Subject subject = SecurityUtils.getSubject();
      // ログイン
      subject.login(token);
      token.clear();
      
      url = "/user/content.jsp";
    } catch (UnknownAccountException ex) {
      ex.printStackTrace();
    } catch (IncorrectCredentialsException ex) {
      ex.printStackTrace();
    } catch (Exception ex) {		
      ex.printStackTrace();
    }

    // リダイレクト
    RequestDispatcher dispatcher =
        getServletContext().getRequestDispatcher(url);
    dispatcher.forward(request, response);
  }
}

ここでは、受け取ったユーザ名とパスワードからTokenを作成し、それを使ってログイン処理を行っています。ログインに成功したら/user/content.jspにリダイレクトします。/user/conten.jspは次のようにしました。単にテキストとログアウトページへのリンクを表示するだけのファイルです。ポイントは、このファイルをauthcフィルタが適用される/user/ディレクトリ以下に配置することです。

<%@ 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>Shiroサンプル - コンテンツ</title>
  </head>
  <body>
    <p>ここは保護されたページです。</p>
    
    <h1>コンテンツページ</h1>
    
    <p><a href="<c:url value='/Logout' />">ログアウト</a></p>
    
  </body>
</html>

ログアウトを行うプログラムはLogout.javaとして、Subjectを取得してlogout()メソッドを呼び出す処理を実行します。次のような感じです。

public class Logout extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
          throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");
    
    String url = "/login.jsp";    // リダイレクト先のURL
    
    // Subjectを取得
    Subject subject = SecurityUtils.getSubject();
    
    if (subject != null) {
      // ログアウト
      subject.logout();
    }

    HttpSession session = request.getSession(false);
    if( session != null ) {
      // セッションの終了
      session.invalidate();
    }

    // リダイレクト
    RequestDispatcher dispatcher =
        getServletContext().getRequestDispatcher(url);
    dispatcher.forward(request, response);
  }

}

web.xmlへの記述内容は次のようになります。shiro.iniは上で掲載したものと同じで、これを/WEB-INF/ディレクトリに配置しましょう。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" 
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
			     http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

  <filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
    <init-param>
      <param-name>configPath</param-name>
      <param-value>/WEB-INF/shiro.ini</param-value> 
    </init-param>
  </filter>

  <servlet>
    <servlet-name>Login</servlet-name>
    <servlet-class>jp.gihyo.toolbox.shiro.Login</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>Logout</servlet-name>
    <servlet-class>jp.gihyo.toolbox.shiro.Logout</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>Login</servlet-name>
    <url-pattern>/Login</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Logout</servlet-name>
    <url-pattern>/Logout</url-pattern>
  </servlet-mapping>
  
</web-app>

以上で完成です。Webサーバにデプロイしてブラウザから/user/content,jspにアクセスしてみましょう。/user/以下はauthcフィルタによって保護されたエリアなので、ログインしていない状態ではl/login.jspにリダイレクトされるはずです。/login.jspでは図1のように表示されるので、ここでユーザ名とパスワードを入力してログインしてください。

図1 ログインページの例
図1 ログインページの例

ログインに成功すると、図2のように/user/content.jspにアクセスできるようになります。

図2 ログインに成功すれば/user/以下のコンテンツにアクセスできる
図2 ログインに成功すれば/user/以下のコンテンツにアクセスできる

カスタムタグライブラリを使う

ShiroにはWebアプリケーションで使うためのJSP/GSP向けのカスタムタグライブラリが用意されています。これを利用することで、認証済みか否かで表示内容を変えるなどといった処理が簡単に記述できるようになります。

たとえば、userタグで囲まれた内容は、認証済みの場合にのみ表示されるようになります。逆に、未認証の場合にのみ表示したい内容がある場合にはgestタグを使うと便利です。次の例は、この2つのタグを使って認証済みか否かで表示内容を変えるものです。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Shiroサンプル - </title>
  </head>
  <body>
    
    <shiro:user>
      <p>こんにちは、<shiro:principal/> さん。</br>
      <shiro:principal/> さんではない場合は
      <a href="<c:url value='/Logout' />">ここ</a>をクリックしてログアウトしてください。</p>
    </shiro:user>

    <shiro:guest>
      <p><a href="<c:url value='/Logout' />">このページ</a>からログインしてください。</p>
    </shiro:guest>
    
  </body>
</html>

認証が行われていないユーザに対しては、図3のように表示されます。一方、認証済みであれば図4のような表示になります。principalタグは、Subjectオブジェクトのユーザ名を取得するカスタムタグです。

図3 guestタグの中身は未認証の場合にのみ表示される
図3 guestタグの中身は未認証の場合にのみ表示される
図4 userタグの中身は認証済みの場合にのみ表示される
図4 userタグの中身は認証済みの場合にのみ表示される

Webアプリケーションにユーザ認証は不可欠ですが、Apache Shiroを使えばこのように簡単にその仕組みを組み込むことができる上、コンテンツごとのアクセス権管理も容易に行えます。今回はユーザ情報の設定にINIファイルのみ使いましたが、各種レルムを導入すれば、LDAPやデータベースをはじめとしたさまざまな認証機構と組み合わせることもできるので、実用性は極めて高いと言えるでしょう。

おすすめ記事

記事・ニュース一覧