Apache Pluto による簡易ポータル開発(ユーザ認証編)

前回の続き。

Plutoについてくるポータルアプリケーションのページ構成は、 src/main/webapp/WEB-INF/pluto-portal-driver.config.xml の静的な記述から構築されているけど、どんなユーザでログインしても全員同じページ構成ではポータルの名が泣くので、ユーザごととはいかないがログインしたユーザのロールごとに異なるページ構成になるよう改造したい。

まずは、その準備として Spring Security を使用してDBテーブルを認証ソースにしたユーザ認証を行うようにする。

データベースの準備

認証と、後ほどページ構成の取得もデータベースからおこなうために、こんな感じのテーブルを設計した。


  • ユーザ、ロール、ページ、ポートレットの4つのテーブル
  • ロール・ページ間は n対n とし、関連テーブルを作成
  • ページ・ポートレット間も n対n とし、関連テーブルを作成

SQLite3 をインストールした上で、以下のコマンドで「portal.sqlite3」ファイルを新しいDBとして作成し、SQLを流してDBを初期化する。

sqlite3 portal.sqlite3
CREATE TABLE USERS
(
	ID INTEGER NOT NULL,
	NAME TEXT NOT NULL UNIQUE,
	PASSWORD TEXT NOT NULL,
	ENABLED BOOLEAN NOT NULL,
	ROLE_ID INTEGER NOT NULL,
	PRIMARY KEY (ID)
);


CREATE TABLE ROLES
(
	ID INTEGER NOT NULL,
	NAME TEXT NOT NULL UNIQUE,
	PRIMARY KEY (ID)
);


INSERT INTO "ROLES" VALUES(1,'ROLE_USER');
INSERT INTO "ROLES" VALUES(2,'ROLE_ADMIN');

INSERT INTO "USERS" VALUES(1,'user','password',1,1);
INSERT INTO "USERS" VALUES(2,'admin','password',1,2);

依存ライブラリの追加

pom.xml に以下の通り依存ライブラリを追加する。

        <!-- Spring Framework / Spring Security -->
        <!-- Pluto が依存しているバージョンが古いため、新しいバージョンを手動指定 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>2.5.6.SEC01</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>2.5.6.SEC01</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>2.5.6.SEC01</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>2.5.6.SEC01</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>2.5.6.SEC01</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>2.5.6.SEC01</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core-tiger</artifactId>
            <version>2.0.5.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- web.xml が 2.3 設定のため 2.3 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.3</version>
            <scope>provided</scope>
        </dependency>

        <!-- SQLite用JDBCドライバ -->
        <dependency>
            <groupId>org.xerial</groupId>
            <artifactId>sqlite-jdbc</artifactId>
            <version>3.6.20</version>
        </dependency>

        <!-- ORMライブラリ(iBATIS) -->
        <dependency>
            <groupId>org.apache.ibatis</groupId>
            <artifactId>ibatis-sqlmap</artifactId>
            <version>2.3.4.726</version>
        </dependency>

Spring Security による認証の設定

現在はサーブレットのFORM認証で tomcat-users.xml を認証ソースにした認証を行っているが、これをデータベースの USERS/ROLES テーブルをもとに行うようにする。

修正するファイルと内容は以下の通り。

src/main/webapp/WEB-INF/web.xml

以下のように contextConfigLocation に applicationContext.xml ファイルも読むよう設定を追加する。

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/pluto-portal-driver-services-config.xml,
        classpath:/applicationContext.xml</param-value>
    </context-param>

また、元々あった plutoPortalDriver フィルタに加えて Spring Security 用のフィルタ設定を追加する。

  <!-- 追加 -->
  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <filter>
    <filter-name>plutoPortalDriver</filter-name>
    <filter-class>org.apache.pluto.driver.PortalDriverFilter</filter-class>
  </filter>

  <!-- 追加 -->
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>plutoPortalDriver</filter-name>
    <url-pattern>/about.jsp</url-pattern>
  </filter-mapping>

そして、不要なFORM認証用の設定( )と、plutoPortalDriverLogout の 要素を削除する。

src/main/resources/applicationContext.xml

以下の内容で作成。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:sec="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd
       ">

  <sec:http auto-config="true">
    <sec:intercept-url pattern="/portal" access="ROLE_USER,ROLE_ADMIN" />
    <sec:intercept-url pattern="/portal/**" access="ROLE_USER,ROLE_ADMIN" />
    <sec:form-login authentication-failure-url="/login.jsp?error=1" login-page="/login.jsp" default-target-url="/portal" />
    <sec:logout logout-success-url="/login.jsp" />
  </sec:http>

  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="org.sqlite.JDBC" />
    <!-- portal.sqlite3 のパスは上でDBを作成したときのパスに変更 -->
    <property name="url" value="jdbc:sqlite:C:/workspace/portal-example/portal.sqlite3" />
  </bean>

  <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocation" value="classpath:sqlmap-config.xml" />
    <property name="dataSource" ref="dataSource" />
  </bean>

  <bean id="userDetailsService" class="com.example.pluto.UserDetailsServiceImpl">
    <property name="sqlMapClient" ref="sqlMapClient" />
  </bean>

  <sec:authentication-provider user-service-ref="userDetailsService">
  </sec:authentication-provider>

</beans>
src/main/resources/sqlmap-config.xml

以下の内容で作成。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
    "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

<sqlMapConfig>

    <settings
        useStatementNamespaces="true"
    />

    <sqlMap resource="sqlmap-portal.xml"/>

</sqlMapConfig>
src/main/resources/sqlmap-portal.xml

以下の内容で作成。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="portal">

  <select id="getUserByName" parameterClass="string" resultClass="java.util.HashMap">
    SELECT users.* FROM users
    WHERE name = #value#
  </select>

  <select id="findRoleNamesByUserName" parameterClass="string" resultClass="string">
    SELECT roles.name FROM roles
      JOIN users ON users.role_id = roles.id
    WHERE users.name = #value#
  </select>

</sqlMap>
src/main/java/com/example/pluto/UserDetailsServiceImpl.java

Eclispe で src/main/java を新しいソース・フォルダとして作成した後、以下の内容で作成する。

package com.example.pluto;

import java.util.List;
import java.util.Map;

import org.springframework.dao.DataAccessException;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;
import org.springframework.security.GrantedAuthorityImpl;
import org.springframework.security.userdetails.User;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException;

public class UserDetailsServiceImpl extends SqlMapClientDaoSupport
implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException, DataAccessException {

        @SuppressWarnings("unchecked")
        Map<String, Object> user = (Map<String, Object>) getSqlMapClientTemplate()
            .queryForObject("portal.getUserByName", username);

        if (user == null) {
            throw new UsernameNotFoundException(
                    String.format("user %s not found", username));
        }

        @SuppressWarnings("unchecked")
        List<String> roles = (List<String>) getSqlMapClientTemplate()
                .queryForList("portal.findRoleNamesByUserName", username);

        GrantedAuthorityImpl[] authorities  = new GrantedAuthorityImpl[roles.size()];
        int i = 0;
        for (String role : roles) {
            authorities [i++] = new GrantedAuthorityImpl(role);
        }

        return new User(
                (String) user.get("NAME"),
                (String) user.get("PASSWORD"),
                ((Integer) user.get("ENABLED")) == 1,
                true,
                true,
                true,
                authorities);
    }
}
src/main/webapp/WEB-INF/themes/pluto-default-theme.jsp

ログアウト用URLを Spring Security 用のものにする。

<!--  これをコメントアウト -->
<!--  <a href="<c:url value='/Logout'/>">Logout</a> -->

<!--  これを追加 -->
      <a href="<c:url value='/j_spring_security_logout'/>">Logout</a>
src/main/webapp/login.jsp

ログイン用URLを Spring Security のものにする。

<!--  これをコメントアウト -->
<!--  <form method="POST" action="j_security_check">  -->

<!--  これを追加 -->
      <form method="POST" action="j_spring_security_check">

実行確認

最初にDBに設定した user/password, admin/password の2つのユーザ名・パスワードの組でログインできることを確認する。