Spring3とApache Shiroを連携させ、かつ独自Filterで認証させる方法


そろそろ忘れそうなのでメモ。
アプリケーションコンテキストでの連携方法です。
Apache Shiroの上に独自のFilterを噛ましてMySQL上のdatabaseに登録したユーザ/パスワードで認証する備忘録です。

アプリケーションコンテキストにApache Shiroのbeanを記載します。
<pre>

\<!-- Apache Shiroの設定 ※公式から持ってきたものをコピペしてカスタマイズしてます-->
  \<bean id="WebApiShiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
      \<property name="securityManager" ref="securityManager"/>
      \<!-- override these for application-specific URLs if you like:
      \<property name="loginUrl" value="/login.jsp"/>
      \<property name="successUrl" value="/home.jsp"/>
      \<property name="unauthorizedUrl" value="/unauthorized.jsp"/> -->
      \<!-- The 'filters' property is not necessary since any declared javax.servlet.Filter bean  -->
      \<!-- defined will be automatically acquired and available via its beanName in chain        -->
      \<!-- definitions, but you can perform instance overrides or name aliases here if you like: -->
      \<!-- <property name="filters">
          \<util:map>
              \<entry key="anAlias" value-ref="someFilter"/>
          \</util:map>
      \</property> -->
      \<property name="filterChainDefinitions">
          \<value>
              /program/myapi.xml = anotherFilter
              /program/myapi.json = anotherFilter
              /mycontext/** = authc
          \</value>
      \</property>
  \</bean>

  \<!-- Define any javax.servlet.Filter beans you want anywhere in this application context.   -->
  \<!-- They will automatically be acquired by the 'shiroFilter' bean above and made available -->
  \<!-- to the 'filterChainDefinitions' property.  Or you can manually/explicitly add them     -->
  \<!-- to the shiroFilter's 'filters' Map if desired. See its JavaDoc for more details.       -->
\<!--
  \<bean id="someFilter" class="..."/>
-->
  \<bean id="anotherFilter" class="org.my.auth.filter.MyFilter">
  \</bean>
  \<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
      \<!-- Single realm app.  If you have multiple realms, use the 'realms' property instead. -->
      \<property name="realm" ref="myRealm"/>
      \<!-- By default the servlet container sessions will be used.  Uncomment this line
           to use shiro's native sessions (see the JavaDoc for more): -->
      \<!-- <property name="sessionMode" value="native"/> -->
  \</bean>
  \<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

  \<!-- Define the Shiro Realm implementation you want to use to connect to your back-end -->
  \<!-- security datasource: -->
  \<bean id="myRealm" class="org.my.auth.relm.MySqlRealm">
      \<property name="MyService" ref="MyService" />
  \</bean>

</pre>

基本構造は上記をコピペしてあげれば良いです。
独自に設定しないといけない箇所は以下となります。

property name="filterChainDefinitions"
<pre>
URLごとに噛ませたい認証方法を定義します。
apache-shiro標準のものも用意されており、それを使用すると未ログインの場合はログイン画面に飛ばすなどの設定も容易に行えます。
今回は独自実装した認証を噛ませたいので、"anotherFilter"としています。
</pre>

bean id="anotherFilter"
<pre>
Springなのでbeanで注入します。
classに独自実装したFilterを設定します。
</pre>

property name="realm"
<pre>
レルムの設定をします。
今回はMySQL上のdatabaseに登録したユーザ/パスワードで認証するためのレルムを作るので、myRelmをrefs属性にセットします。
</pre>

bean id="myRealm"
<pre>
Springなので、やっぱりDIさせます。
MySqlレルムなので、class属性に"org.my.auth.relm.MySqlRealm"をセットしています。
MySqlレルムの中でサービスを使いまわしたいので、MyServiceをインジェクションするように設定しています。
</pre>

これでApplicationContextの設定は完了です。
実装に移ります。

■org.my.auth.filter.MyFilterクラス
<pre>

/**
 * MySQL認証用に作成した独自フィルタです。
 */
@Scope("prototype")
@Component
public class MyFilter extends AccessControlFilter{

    public MyFilter(){
        super();
    }
    
    /**
     * アクセス許可時の処理
     * カスタマイズ用の認証処理です。
     * @param req リクエスト情報
     * @param rsp レスポンス情報
     * @param obj 付加情報
     * @throws Exception 例外
     * @return boolean true:アクセス許可
     */
    @Override
    protected boolean isAccessAllowed(final ServletRequest req, final ServletResponse rsp
            , final Object obj) throws Exception {
        
        //ユーザIDとパスワードを使ってログイン実行する。
        String userName = req.getParameter("user_name");
        
        String password = req.getParameter("password");
        
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
        final Subject subject = SecurityUtils.getSubject();
        
        boolean flg = false;
        
        try{
            
            subject.login(token);
            
            return true;
            
        //loginメソッドで認証失敗すると例外が飛んできます
        }catch(AuthenticationException e){
            
            return false;

        }
    }

    /**
     * アクセス拒否時の処理
     * @param req リクエスト情報
     * @param rsp レスポンス情報
     * @throws Exception 例外
     * @return boolean false:アクセス許拒否
     */
    @Override
    protected boolean onAccessDenied(final ServletRequest req, final ServletResponse rsp)
        throws Exception {
        //Apache-shiroを介した別の認証フィルターで失敗した場合に、飛ばされます。
        //1個しか指定していないので、今回はここには飛んできませんので、実装しません。
        return false;
    }
    
}
</pre>

Scopeアノテーションに"prototype"を設定しているのは、
Springはデフォルトシングルトンなので、同時に異なるユーザがアクセスしてきた場合に、
後勝ちされてしまうためです。
（先にアクセスしてきたユーザが、後からアクセスしてきたユーザとして認証ＯＫとなる可能性がある。）
prototype指定しておくことで、毎回newされてインジェクションされます。
※Springの説明ではないため、Scopeアノテーションの設定値については割愛します。

apache-shiroのフィルタを独自実装する場合は、AccessControlFilterクラスを継承します。
継承すると、isAccessAllowedとonAccessDeniedメソッドを実装するように言われるので実装します。
isAccessAllowedメソッドに認証処理を入れればＯＫです。
テスト用なので簡単にするため、HTTP GETパラメータにuser_nameとpassword（SHA-256ハッシュ化）を詰めて受け取る作りとしています。
取ったものは、UsernamePasswordTokenクラスを使ってトークンを取得します。
トークンはSubject（apache-shiroの認証の仕組みにサブジェクトがあり、そのサブジェクトです。）を取ってloginメソッドに渡すだけです。
ログインに成功するとvoidなのでメソッドを通過します。
失敗した場合は、例外が飛んでくるので、例外ブロックに認証ＮＧ時の処理を書きます。
ちなみに、MySqlRealmクラスを通しますが、ここでExceptionを投げた場合についても、
AuthenticateExceptionに全て変換されるので、独自の例外などは投げれませんでした。

■org.my.auth.relm.MySqlRealmクラス
<pre>

/**
 * MySQL認証用のレルム
 */
@Scope("prototype")
@Component
public final class MySqlRealm extends JdbcRealm {

    @Autowired
    private MyService myService = null;

    public void setMyService(final MyService myService){
        this.myService = myService;
    }
    
    public MyService getMyService(){
        return myService;
    }
    
    /**
     * 認証処理
     * @param token 認証トークン
     * @return AuthenticationInfo
     * @throws AuthenticationException Apache Shiroからの例外
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
        throws AuthenticationException {

        //トークンからユーザ名を取得する
        final UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        final String userName = upToken.getUsername();
        final String password = String.valueOf(upToken.getPassword());

        //DBからユーザ情報を取得する。
        UserInfo user = myService.getUserInfo(userName, password);

        Sha256CredentialsMatcher credentialsMatcher = new Sha256CredentialsMatcher();
        super.setCredentialsMatcher(credentialsMatcher);
        credentialsMatcher.setHashIterations(1);
        
        //パスワードが改ざんされていないかをチェック
        final Sha256Hash credentials = Sha256Hash.fromBase64String(user.getPassword());
        credentials.setSalt(ByteSource.Util.bytes(Base64.decode(user.getSalt())));
        
        final SimpleAuthenticationInfo info 
            = new SimpleAuthenticationInfo(userName, credentials, getName());
        info.setCredentialsSalt(credentials.getSalt());
        
        return info;
    }

}
</pre>

MySQLでレルムを作るときは、JdbcRealmというクラスを継承します。
※多分PostgreSQLのレルムを作っても、このクラスを継承すればいけるはずです。

MyServiceクラスをSpringにDIしてもらうため、AUTOWIREDアノテーションを付けて、setterメソッドを作っています。
フィールドに直接インジェクションさせれますが抵抗あるので、私は毎度こうしています。

JdbcRelmを継承すると、doGetAuthenticationInfoメソッドを実装するように言われるので、実装します。
MyFilterで作ったトークンを渡されるので、これと、MyServiceを使って取ったユーザ情報を比較します。

HTTP GETで渡されたパスワードが改竄されていないかをチェックします。
DB上には、SHA-256化したハッシュ文字列をそのまま保存しておくのではなく、
ソルトを取った上で保存しています。
なので、HTTP GETのパスワードを、SHA-256文字列 + ソルトで当てに行きます。
改竄チェック自体は、SimpleAuthenticationInfoクラスに渡して上げ、
doGetAuthenticationInfoメソッドの戻りにセットしてあげれば、
shiro側でよろしくチェックしてくれ、改竄されていればExceptionが上がるという仕組みです。
これでSpring3とApache Shiroを連携させ、かつ独自Filterで認証させる実装が完成しました。

Apache-Shiroは認証フレームワークですが、ライブラリも充実しているので
単にライブラリとして使うのも良いです。
ライブラリだけ使いたい場合は、ライブラリだけを入れたjarもあったかと思います。