2014年12月16日火曜日

Sinatra + SSL

Found a better way to enable SSL in Sinatra than "Sinatra+Thin+SSL". This is a generic way and does not require Thin.

2014年10月1日水曜日

Sinatra + Thin + SSL

Finally found a way to enable SSL in Sinatra + Thin.

# [2014/12/16]
# Found a better way.
# See "Sinatra + SSL".


Start this script (sinatra+thin+ssl.rb) then type:
curl -k https://localhost/
and you will see "Hello, SSL."

2014年9月20日土曜日

MissingTableException on Google Cloud SQL

If you are using JDO to access Google Cloud SQL and suffering from MissingTableException like below:

org.datanucleus.store.rdbms.exceptions.MissingTableException: Required table missing : "`TABLE NAME`" in Catalog "" Schema "". DataNucleus requires this table to perform its persistence operations. Either your MetaData is incorrect, or you need to enable "datanucleus.autoCreateTables"

and if your table names contain lowercase letters, probably the issue can be solved by setting "datanucleus.identifier.case" properly.

"JDO : Datastore Identifiers" says "By default, DataNucleus will capitalise names". Therefore, if your table names contain lowercase letters, "datanucleus.identifier.case" should be set explicitly.

The valid values for "datanucleus.identifier.case" are UpperCase, LowerCase or PreserveCase (or MixedCase; See Change "datanucleus.identifier.case" 'PreserveCase' to be 'MixedCase' to be match internal namings). So, for example, your jdoconfig.xml will have to have the entry like the following.

<property name="datanucleus.identifier.case" value="LowerCase"/>

If you are developing your GAE application on Windows or Mac OS X, you may not encounter this case sensitivity problem until you deploy your application onto GAE. See "9.2.2 Identifier Case Sensitivity" (MySQL Reference Manual) for details.

2014年7月19日土曜日

JDO:カラムサイズは十分大きいのにデータが保存できないという問題を解消する方法

  JDO を使っているときに表示される次のようなエラーメッセージは、

Attempt to store value "{DATA}" in column "`{COLUMN-NAME}`" that has maximum length of 255. Please correct your data!

保存しようとしているデータのサイズが、対応するデータベーステーブルのカラムのデータサイズよりも大きいということを意味している。しかし、カラムサイズが十分大きくてもこのエラーが起きることがある。例えば、対応するカラムの型を TEXT で定義しているにもかかわらず(MySQL 的には TEXT は 65535 まで OK であるにもかかわらず)、上記のようなエラーが出てしまうことがある。

  この問題は、@Column アノテーションに length を追加して、最大長を指定すれば解消するようだ。具体的には、

@Persistent
@Column(name = "comment")
private String comment;

となっているところを、

@Persistent
@Column(name = "comment", length = 65535)
private String comment;

というような具合に変更する。


2014年7月10日木曜日

JDO Enum Mapping

  Java Data Objects 3.0 (JSR 243) の仕様によると、Enum の値をストレージに永続化する際、ストレージでの型が数値型であれば Enum.ordinal() メソッドの返す値が、文字列型であれば Enum.name() メソッドの返す値が用いられるとのこと。 (15.1 Column Elements / Mapping enums)

  Enum にマッピングされるカラムのデフォルトの jdbc-type は VARCHAR なので、ordinal() の値を使いたいときは、当該カラムに対して明示的に jdbc-type を指定したほうがよい。有効な jdbc-type は次のとおりで、全て大文字か全て小文字かのどちらかで指定する。 (18.4 ELEMENT column)

CHAR, VARCHAR, LONGVARCHAR, NUMERIC, DECIMAL, BIT, TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, BINARY, VARBINARY, LONGVARBINARY, DATE, TIME, TIMESTAMP

  DataNucleus は、name()ordinal() ではない別の値で Enum を永続化する拡張を提供している。下記は、「JDO : Persistable Field Types」 に挙げられている例である。

public enum MyColour 
{
    RED((short)1), GREEN((short)3), BLUE((short)5), YELLOW((short)8);

    private short value;

    private MyColour(short value)
    {
        this.value = value;
    }

    public short getValue() 
    {
        return value;
    }

    public static MyColour getEnumByValue(short value)
    {
        switch (value)
        {
            case 1:
                return RED;
            case 3:
                return GREEN;
            case 5:
                return BLUE;
            default:
                return YELLOW;
        }
    }
}
@Extensions({
    @Extension(vendorName="datanucleus",
               key="enum-getter-by-value", value="getEnumByValue"),
    @Extension(vendorName="datanucleus",
               key="enum-value-getter", value="getValue")
   })
MyColour colour;

  name() で永続化する場合は 「"RED", "GREEN", "BLUE", "YELLOW"」、 ordinal() で永続化する場合は 「0, 1, 2, 3」 であるが、上記の例のようにすれば、「1, 3, 5, 8」 という値で永続化される。

  ただ、ここに誤りやすいポイントがある。DataNucleus の拡張を使う場合、値のデータ型は short でなければならないようだ。例えば上記の実装で getEnumByValue(short value) となっているところを getEnumByValue(int value) に変更すると、拡張機能が動作しない。逆にデータサイズを小さくする getEnumByValue(byte value) でも、やはり動作しない。ただ、ストレージでの型は必ずしも SMALLINT でなくてもよいようだ。

  例えば、ClientType という enum を定義し、PUBLICCONFIDENTIAL というエントリーを持たせ、それぞれ 1, 2 という値で永続化したい場合、その実装は次のようになる。


public enum ClientType 
{
    PUBLIC((short)1),
    CONFIDENTIAL((short)2); 
 
    private static final ClientType[] mValues = values(); 
    private final short mValue;
 
    private ClientType(short value)
    {
        mValue = value;
    }
 
    public short getValue()
    {
        return mValue;
    }
 
    public static ClientType getByValue(short value)
    {
        if (value < 1 || mValues.length < value)
        {
            return null;
        }

        return mValues[value - 1];
    }
}

  この ClientType 型のフィールドを持つ ClientEntity クラスを定義するとすれば次のような感じになる。


@PersistenceCapable(
        identityType = IdentityType.APPLICATION,
        table        = "client",
        detachable   = "true")
public class ClientEntity 
{
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    @Column(name = "number")
    private int number;
 
    @Persistent(defaultFetchGroup = "true")
    @Column(name = "client_type", jdbcType = "TINYINT")
    @Extensions({
        @Extension(vendorName = "datanucleus",
                   key = "enum-getter-by-value", value = "getByValue"),
        @Extension(vendorName = "datanucleus",
                   key = "enum-value-getter", value = "getValue")
    })
    private ClientType clientType;

    public int getNumber()
    {
        return number;
    }

    public ClientType getClientType()
    {
        return clientType;
    }

    public void setClientType(ClientType clientType)
    {
        this.clientType = clientType;
    }
}

ClientEntity クラスに対応するテーブル "client" を MySQL で定義するならば、次のような感じ。

CREATE TABLE client
(
    number INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    client_type TINYINT UNSIGNED NOT NULL DEFAULT 1
);

2014年4月19日土曜日

2 の 256 乗を計算してみた

2256 を十進数で表すと 78 桁で、無量大数の十億倍くらいの値であることが分かりました。 ちゃんとした方法でランダムに生成した 256 ビットのハッシュ値が偶然ぶつかるかもしれないなんてことを心配するのは馬鹿げていることを理解しました。いや、馬鹿げているのは理解していたのですけど、実際に計算してみると、どれだけ途方もなく馬鹿げているのかが分かった、という話です。

どれくらい大きな数字なのか例えで考えてみます。
  1. 太陽の寿命が来るまで今から 50 億年のあいだ、
  2. 100 億人の人が、
  3. 毎ナノ秒 1 兆個の数値を消費していく活動があり、
  4. その活動を、太陽系が属するこの銀河系の全ての星(~2,000 億くらい)でおこなうと、
消費される数字の個数は、50 億年 × 100 億人 × 100000000 ナノ秒 × 60 秒 × 60 分 × 24 時間 × 365 日 × 1 兆個 × 2000 億個(星) で 59 桁の数字になります。こんなに頑張って消費しても、50 億年後 (太陽が赤色巨星になって太陽系が崩壊する頃) に消費し終わっているのは、(59 桁) / (78 桁) = 10-19、つまり全体の 100 京分の 1 です。

ちなみに、Java の long は 64 ビットで 10 進数だと 20 桁 (2000 京弱)。10 億人が毎秒消費なら1000 年くらいで使い切る計算。秒間 10 億を千年。それを超えるような想定をしなければならないシステムを作ることはないでしょうね。推測可能でよい ID なら、long で連番振っておけば十分でしょうね。


21 = 2
22 = 4
23 = 8
24 = 16
25 = 32
26 = 64
27 = 128
28 = 256
29 = 512
210 = 1,024
211 = 2,048
212 = 4,096
213 = 8,192
214 = 16,384
215 = 32,768
216 = 65,536
217 = 131,072
218 = 262,144
219 = 524,288
220 = 1,048,576 (7 桁)
= 104 万 8576
221 = 2,097,152 (7 桁)
= 209 万 7152
222 = 4,194,304 (7 桁)
= 419 万 4304
223 = 8,388,608 (7 桁)
= 838 万 8608
224 = 16,777,216 (8 桁)
= 1677 万 7216
225 = 33,554,432 (8 桁)
= 3355 万 4432
226 = 67,108,864 (8 桁)
= 6710 万 8864
227 = 134,217,728 (9 桁)
= 1 億 3421 万 7728
228 = 268,435,456 (9 桁)
= 2 億 6843 万 5456
229 = 536,870,912 (9 桁)
= 5 億 3687 万 912
230 = 1,073,741,824 (10 桁)
= 10 億 7374 万 1824
231 = 2,147,483,648 (10 桁)
= 21 億 4748 万 3648
232 = 4,294,967,296 (10 桁)
= 42 億 9496 万 7296
248 = 4,281,474,976,710,656 (16 桁)
= 4281 兆 4749 億 7671 万 656
264 = 18,446,744,073,709,551,616 (20 桁)
= 1844 京 6744 兆 737 億 955 万 1616
2128 = 340,282,366,920,938,463,463,374,607,431,768,211,456 (39 桁)
= 340 澗 2823 溝 6692 穣 938 じょ 4634 垓
6337 京 4607 兆 4317 億 6821 万 1456
2256 = 115,792,089,237,316,195,423,570,985,008,687,907,853,
269,984,665,640,564,039,457,584,007,913,129,639,936 (78 桁)
= 1157920892 無量大数
3731 不可思議 6195 那由他 4235 阿僧祇
7098 恒河沙 5008 極 6879 載 785 正
3269 澗 9846 溝 6564 穣 564 じょ 394 垓
5758 京 4007 兆 9131 億 2963 万 9936

Blogger の都合でサロゲートペアの文字が化けるため、平仮名で表記。 Blogger もうやだ・・・。

2014年3月17日月曜日

AccessControlException when using local MySQL for Google Cloud SQL

  If you are developing an application for Google App Engine + Google Cloud SQL and using local MySQL during development, you may encounter AccessControlExeption like below.
java.security.AccessControlException:
    access denied ("java.lang.RuntimePermission" "modifyThreadGroup")
  This exception is raised when your application tries to use Thread because Google App Engine does not allow application to use Thread.

  Reading the stack trace, you will find the point where Thread is created.
at java.lang.Thread.<init>(Thread.java:444)
at java.util.TimerThread.<init>(Timer.java:499)
at java.util.Timer.<init>(Timer.java:101)
at java.util.Timer.<init>(Timer.java:146)
at com.mysql.jdbc.ConnectionImpl.getCancelTimer(ConnectionImpl.java:392)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2195)
  From the information above, it can be guessed that a Thread instance will not be created if PreparedStatement.executeInternal method does not call ConnectionImpl.getCancelTimer method.

  Below is an excerpt from the source code of executeInternal method.
if (locallyScopedConnection.getEnableQueryTimeouts() &&
    this.timeoutInMillis != 0 &&
    locallyScopedConnection.versionMeetsMinimum(5, 0, 0))
{
    timeoutTask = new CancelTask(this);
    locallyScopedConnection
        .getCancelTimer()
        .schedule(timeoutTask, this.timeoutInMillis);
  The code snippet indicates that getCancelTimer method is not called if timeouts are not set.

  So, check your jdoconfig.xml. If DatastoreReadTimeoutMillis and/or DatastoreWriteTimeoutMillis are configured, comment them out during development.

2014年3月13日木曜日

MySQL: Error 1067 Process terminated unexpectedly

  If MySQL service fails to start and reports "Error 1067", it indicates that the configuration file my.ini contains errors. 1067 is a generic error code, so you cannot tell which configuration parameter is the cause. Therefore, to find wrong configuration parameters, you have to repeat comment in/out configuration parameters and start/stop MySQL service.

  In my case, configuration parameters which had problems were slow_query_log_file and log-error. Log file paths set to these parameters had to exist in advance. Why doesn't MySQL service create these files if they don't exist yet? I don't know why. Anyway, I created empty log files and could start MySQL service successfully.

My environment
Windows 7 Home Premium Service Pack 1, 64-bit
MySQL Server 5.6
my.ini location = C:\ProgramData\MySQL\MySQL Server 5.6\my.ini



MySQL: エラー 1067: プロセスを途中で強制終了しました。

  MySQL サービスが起動に失敗して「エラー 1067」を報告してきた場合、それは設定ファイル my.ini にエラーがあることを示している。1067 は汎用エラーコードなので、どの設定パラメーターが原因なのかは分からない。そのため、誤った設定パラメーターを見つけるためには、設定パラメーターのコメントイン/アウトと MySQL サービスの起動/停止を繰り返さなければならない。

  私の場合は、slow_query_log_filelog-error という設定パラメーターに問題があった。これらのパラメーターに設定するログファイルのパスは、あらかじめ存在している必要があった。なぜ MySQL サービスは、これらのファイルがまだ存在していなければ作成する、という動作をしないのだろうか? 理由は分からない。いずれにせよ、空のログファイルを作ったら MySQL サービスの起動に成功した。

私の環境
Windows 7 Home Premium Service Pack 1, 64-bit
MySQL Server 5.6
my.ini の場所 = C:\ProgramData\MySQL\MySQL Server 5.6\my.ini


2014年3月5日水曜日

Google App Engine Tips

  1. Eclipse で com.google.appengine.archetypes:guestbook-archetype を指定して GAE のテンプレートプロジェクトを作ると、.classpath の中に <classpathentry> が大量に挿入されるが、絶対パスで書かれるので、GAE SDK をアップデートするときに都合が悪い。 .classpath を手作業で編集して次の内容で置き換えたほうがよい。
    <classpathentry kind="con"
      path="com.google.appengine.eclipse.core.GAE_CONTAINER"/>
    
  2. .classpath の中で、GAE_CONTAINER を MAVEN2_CLASSPATH_CONTAINER より前に置いておかないと、.m2 の中にある GAE ライブラリが先に検索で見つかってしまい、変なエラーが出ることがある。あと、Java Build Path の Order and Export でも GAE のほうを上にしておかないとダメみたい。
  3. guestbook-archetype で GAE テンプレートプロジェクトを作ると eclipse-launch-profiles というサブディレクトリができ、その中に *.launch ファイルができる。 これらのファイルの中の M2_RUNTIME の値は EMBEDDED になっているが、これは DEFAULT に書き換えたほうがよい。Eclipse の Windows -> Preferences -> Maven -> Installations の内容に EMBEDDED が無い場合、エラーとなってしまう。
    <stringAttribute key="M2_RUNTIME" value="DEFAULT"/>
    
    ちなみに、eclipse-launch-profiles の中にあるファイルは、プロジェクトを右クリックして Properties -> Run As -> Maven build とすると出てくる。
  4. logging.properties で formatter を設定しても、うまくいかない。 開発サーバーなら、次のようなことをすれば独自のフォーマッターを使うようにすることもできるが、実サーバーだとこのやり方もうまくいかない。
    Logger logger = Logger.getLogger(...);
    
    for (Logger l = logger; l != null; l = l.getParent())
    {
        Handler[] handlers = l.getHandlers();
    
        // If the logger does not have any handler.
        if (handlers == null || handlers.length == 0)
        {
            continue;
        }
    
        // For each handler of the logger.
        for (Handler handler : handlers)
        {
            // If the handler's formatter is not ours.
            if (handler.getFormatter() != formatter)
            {
                // Set (or replace) the formatter.
                handler.setFormatter(formatter);
            }
        }
    }
    
    Stack Overflow にも "How can I change the logging format in Google AppEngine" という質問が出ているが、結局解決策は無いようだ。
  5. GAE SDK 1.9.0 を設定し、プロジェクトを実行すると、Maven が appengine-java-sdk-1.9.0.zip のダウンロードに失敗して次のようなエラーを吐く。
    Return code is: 503 , ReasonPhrase:backend read error.
    
    たぶんファイルが大き過ぎるのだろう。こうなった場合は、appengine-java-sdk-1.9.0.zip ファイルと appengine-java-sdk.1.9.0.pom を Maven Central Repository からダウンロードし、次のようにして手作業でローカル Maven リポジトリにインストールするほかない。
    mvn install:install-file \
      -Dfile=appengine-java-sdk-1.9.0.zip \
      -DpomFile=appengine-java-sdk-1.9.0.pom
    
  6. プロジェクトを右クリックして Properties -> Run As -> Maven build とし、 UpdateApplication でアプリケーションを GAE 実環境にはじめてアップロードしようとすると、 デフォルトブラウザが立ち上がり、Google App Engine サイトへのログイン後にキーが表示される。 このキーを Eclipse に入力すると、以後、ログイン無しでもアップロードできるようになる。 ただし、デフォルトブラウザで使っている Google アカウントと、GAE アプリケーション開発で使っている Google アカウントが別々だと、面倒なことになる。表示されたキーを Eclipse に入力しても、 "This application does not exist (app_id=u'xxx')" というエラーが出てしまう。 エラーが出るくらいならばいいが、再度試みると、前回入力したキーが有効なため、 キーを再入力するチャンスも与えられず、アップロードに失敗してしまう。 こうなった場合は、次の手順をおこなう。

    1. デフォルトブラウザから Google アカウントを一度ログアウトするか、他のブラウザを一時的にデフォルトブラウザに設定する。
    2. ~/.appcfg_oauth2_tokens_java (または ~/.appcfg_cookies) を削除する。
    3. 再度 UpdateApplication を実行する。
    4. 表示されたブラウザに GAE アプリケーション開発用の Google アカウントでログインしてキーを入手する。
    5. キーを Eclipse に入力する。
  7. SystemProperty.environment.value() の値を調べると、実行環境が開発サーバーなのか実サーバーなのかを識別することができる。こんなクラスを用意しておくと便利。
    public class Env
    {
        public static boolean isProduction()
        {
            return SystemProperty.environment.value() ==
                    SystemProperty.Environment.Value.Production;
        }
    
    
        public static boolean isDevelopment()
        {
            return SystemProperty.environment.value() ==
                    SystemProperty.Environment.Value.Development;
        }
    }
    
  8. クライアントからのリクエストに含まれる Content-Encoding ヘッダーは GAE により取り除かれるので、GAE アプリケーションからは参照できない。かわりに、GAE が Content-Encoding ヘッダーの内容に合わせて勝手に適宜 gzip などしてくれる。(詳しくは Java Runtime Environment, Content-Encoding を参照のこと)
  9. アプリを実行すると、ClassNotFoundException が出て困った。かなり調べた結果、WEB-INF/lib 内の幾つかの JAR ファイルが壊れていることが分かった。なぜ壊れるのか。かなり調べた結果、maven-war-plugin がファイルコピー時に .jar ファイルに対してフィルタリング処理を実行していることが原因のようだと分かった。maven-war-plugin がフィルタリング処理を行わない拡張子のデフォルトリストは jpg, jpeg, gif, bmp, png とのこと。.jar が含まれていない。maven-war-plugin の Filtering の説明を参考に、.jar をフィルタリング処理から外さなければならない。結果、pom.xml の該当部分は次のようになる。
    <build>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.4</version>
        <configuration>
          <nonFilteredFileExtensions>
            <nonFilteredFileExtension>jar</nonFilteredFileExtension>
          </nonFilteredFileExtensions>
          <archiveClasses>true</archiveClasses>
          <webResources>
            <!-- in order to interpolate version from pom into appengine-web.xml -->
            <resource>
              <directory>${basedir}/src/main/webapp/WEB-INF</directory>
              <filtering>true</filtering>
              <targetPath>WEB-INF</targetPath>
            </resource>
          </webResources>
        </configuration>
      </plugin>
    
    ちなみに、JAR ファイルが壊れているかどうかは、WEB-INF/lib ディレクトリに移動して次のようなコマンドを実行すると分かる。
    for i in *.jar; do echo === $i ===; (jar tf $i > /dev/null); done
    
    JAR ファイルが壊れていると、次のような表示になる。
    === datanucleus-appengine-1.0.10.final.jar ===
    java.util.zip.ZipException: error in opening zip file
            at java.util.zip.ZipFile.open(Native Method)
            at java.util.zip.ZipFile.<init>(ZipFile.java:215)
            at java.util.zip.ZipFile.<init>(ZipFile.java:145)
            at java.util.zip.ZipFile.<init>(ZipFile.java:116)
            at sun.tools.jar.Main.list(Main.java:1004)
            at sun.tools.jar.Main.run(Main.java:245)
            at sun.tools.jar.Main.main(Main.java:1177)
    
  10. ClassNotFoundException でも、次のものは別問題。
    java.lang.ClassNotFoundException:
      org.datanucleus.api.jdo.JDOPersistenceManagerFactory
    
    これは、JDO 3.0 を使おうと思って jdoconfig.xml 内で PersistenceManagerFactoryClass に org.datanuleus.api.jdo.JDOPersistenceManagerFactory を指定したのに、それが見つからないということ。JDO 2.3 から JDO 3.0 への変更時、 PersistenceManagerFactoryClass は org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory から org.datanuleus.api.jdo.JDOPersistenceManagerFactory へと変更されたものの(参考: Changes to Configuration File)、 その org.datanuleus.api.jdo.JDOPersistenceManagerFactory を含むライブラリを取り込んでいないということ。 UpgradingToVersionTwo を参考にしつつも、最終的には pom.xml に次のエントリーを追加することで ClassNotFoundException は無くなった。
    <dependency>
      <groupId>javax.jdo</groupId>
      <artifactId>jdo-api</artifactId>
      <version>3.0.1</version> <!-- 09-Feb-2012 -->
    </dependency> 
    <dependency>
      <groupId>javax.transaction</groupId>
      <artifactId>transaction-api</artifactId>
      <version>1.1</version> <!-- 22-Jan-2010 -->
    </dependency> 
    <dependency>
      <groupId>com.google.appengine.orm</groupId>
      <artifactId>datanucleus-appengine</artifactId>
      <version>2.1.2</version> <!-- 15-Feb-2013 -->
    </dependency> 
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-core</artifactId>
      <version>3.1.3</version> <!-- 12-Nov-2012 -->
    </dependency>
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-api-jdo</artifactId>
      <version>3.1.3</version> <!-- 12-Nov-2012 -->
    </dependency>
    
  11. Google Cloud SQL と一緒に使っていて、実行時に 「There is no available StoreManager of type "jdbc". Make sure that you have put the relevant DataNucleus store plugin in your CLASSPATH and if defining a connection via JNDI or DataSource you also need to provide persistence property "datanucleus.storeManagerType"」 というエラーが出たら、pom.xml に datanucleus-rdbms を追加してみる。
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-rdbms</artifactId>
      <version>3.1.3</version> <!-- 12-Nov-2012 -->
      <scope>runtime</scope>
    </dependency>
    
  12. 開発中、Google Cloud SQL の代わりにローカル環境の MySQL を使う設定にしていて、「Caused by: org.datanucleus.store.rdbms.datasource.DatastoreDriverNotFoundException: The specified datastore driver ("com.mysql.jdbc.Driver") was not found in the CLASSPATH. Please check your CLASSPATH specification, and the name of the driver.」 という感じで MySQL 用のドライバーが見つからない、というエラーが出たら、pom.xml に mysql-connector-java を追加してみる。
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.29</version>
      <scope>runtime</scope>
    </dependency>
    
  13. 開発中に MySQL を使っていて、AccessControlException が発生し、その原因が Thread を生成したせいであるならば、DatastoreReadTimeoutMillis や DatastoreWriteTimeoutMillis の設定を jdoconfig.xml からコメントアウトすることで問題を回避できる場合がある。詳細はこちら → AccessControlException when using local MySQL for Google Cloud SQL
  14. Enhance フェーズで「コマンドラインが長すぎます。」 (The command line is too long.) というエラーが出た場合、一般的には
    <plugin>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-maven-plugin</artifactId>
      <configuration>
        <fork>false</fork>
      </configuration>
    </plugin>
    
    を追加することで解決するが、GAE 開発のときは解決しない。appengine-maven-plugin が絡んでいるから。そこで、
    <plugin>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-maven-plugin</artifactId>
      <version>${appengine.target.version}</version>
      <configuration>
        <fork>false</fork>
      </configuration>
    </plugin>
    
    としても、こちらでも解決しない。appengine-maven-plugin が <configuration> 内の <fork>false</fork> を認識しないから。

    しかし、datanucleus のソースコードを読んで解決策を見つけた人がいた(appengine-maven-plugin Issue 49)。<properties> 内に <fork>false</fork> を追加すればよいらしい。
    <properties>
      <fork>false</fork>
    </properties>
    
  15. JDO をやっていて、「Query for candidates of クラス名 and subclasses resulted in no possible candidates」 というようなエラーが出た場合、直接的な原因はおそらく、enhance がうまくいっていないこと。で、間接的な原因はおそらく、Eclipse の 「properties -> Google -> App Engine -> ORM」 の設定で何かミスをしていること。あるフォルダー以下のファイルを全部 enhance の対象とするとの意図で 「.../folder」 と書いても、どうもうまくいかないようだ ("specify file, folder or pattern" と書いてあるのに・・・)。かわりに 「.../folder/*.java」 という具合に指定すれば、フォルダー以下の *.java ファイルが確実に enhance の対象になる。 ただし、enhance がうまく動かない理由はほかにもあると思われる。

    次のコマンドにより手作業で enhance してみるという方法もある。
    $ mvn appengine:enhance
    
    datanucleus.log というログファイルができるので、その中身を確認する。
  16. 開発サーバーのポート番号を変更したいときは、appengine-maven-plugin の configuration で port を指定する。
    <plugin>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-maven-plugin</artifactId>
      <version>${appengine.target.version}</version>
      <configuration>
        <port>8880</port>
      </configuration>
    </plugin>
    
  17. 認可された App Engine アプリから Cloud SQL にアクセスする場合、DB パスワードを指定すると、逆にエラーになってしまう。 「App Engine Java Servlet does not connect to Cloud SQL」に対する回答を参照のこと。
  18. Jersey (JAX-RS 実装) を使い、クライアントエラー時 (= HTTP ステータスコードが 400 番台の時) も JSON を返すエンドポイントを実装したが、「GAE 開発サーバーでは期待通りの動作をするが実際の GAE では HTML が返される」、という問題にぶちあたった。最初は GAE の問題だと思っていたが、JAX-RS ではなく素の Servlet では問題が起きないことから、Jersey のせいではないかと思い、Jersey のソースコードを読みはじめたところ、すぐに直感が働いた。Jersey の サーバー設定項目の一つである "jersey.config.server.response.setStatusOverSendError" を true に設定したところ、問題が解決した。下記は web.xml からの抜粋。
    <servlet>
      <servlet-name>API</servlet-name>
      <servlet-class>
        org.glassfish.jersey.servlet.ServletContainer
      </servlet-class>
      ......
      </init-param>
      <init-param>
        <param-name>
          jersey.config.server.response.setStatusOverSendError
        </param-name>
        <param-value>true</param-value>
      </init-param>
    </servlet>
    
    詳細は StackOverflow の質問「GAE/J changes Content-Type from JSON to HTML when the status code is 4xx」に対する回答 (自己解決) を参照のこと。
  19. Google Cloud SQL に JDO を使ってアクセスしていたが、admin テーブルを参照すると、「org.datanucleus.store.rdbms.exceptions.MissingTableException: Required table missing : "`ADMIN`" in Catalog "" Schema "". DataNucleus requires this table to perform its persistence operations. Either your MetaData is incorrect, or you need to enable "datanucleus.autoCreateTables"」という例外が出た。しかし、当該テーブルは存在するし、他のテーブルには同じ方法でアクセスできていた。当該テーブル用のエンティティークラスが enhance されていることは javap プログラムで確認していた。なぜそのテーブルが存在していないとみなされるのかが分からない。

    時間を使っていろいろ試行錯誤したがうまくいかないので、エラーメッセージに素直に従って、jdoconfig.xml で datanucleus.autoCreateTables を true に設定したら、
    <property name="datanucleus.autoCreateTables" value="true"/>
    
    MissingTableException は出なくなったが、JDO 経由で当該テーブルにアクセスすると、データが存在しないと言われる。様子がおかしいので Google Cloud SQL に mysql コマンドでログインして show tables をすると、admin テーブルに加えて、ADMIN テーブルが作成されていた。

    これは、識別子の case sensitivity の問題だ。"JDO : Datastore Identifiers" によると、JDO はデフォルトで識別子を大文字にしてしまうそうで、もしも識別子に小文字を使っていて、かつ、MySQL を動かしている環境のファイルシステムが大文字・小文字を区別する場合、JDO からはテーブルが発見できない。このようなときは、識別子の case sensitivity を制御する "datanucleus.identifier.case" 変数を適切にセットする必要がある。私の場合は識別子に全て小文字を使っていたので、次のような設定を jdoconfig.xml に追加した。
    <property name="datanucleus.identifier.case" value="LowerCase"/>
    
    詳細は "MissingTableException on Google Cloud SQL" を参照のこと。
  20. ローカルマシンで動く GAE 開発サーバーに ASCII 以外の文字 (日本語など) をフォーム送信すると、正しくパースされず、エラーになるようだ。開発サーバーのログには「no viable alternative at character」云々のエラーメッセージが出る。ちなみに、「no viable alternative at character」というメッセージは、org.antlr.runtime.Lexer.java というソースコードから来ていると思われる。GAE 開発サーバーは Jetty 6 というかなり古い Jetty をベースに作られているようなので、いろいろ不具合が残っているのだろう。なお、本番環境の GAE ではこの問題は起こらない。
  21. GAE 関係なく一般的な話。JDO の PersistenceManagerFactory は生成にコストがかかるので、一度インスタンスを作ってそれを使いまわすことが推奨されている。そういうわけで、コード上ではシングルトンインスタンスにする。でも、アプリのインスタンスが複数存在すると、同時に PersistenceManagerFactory が複数存在することになる。ここで問題になるのが JDO の Level 2 Cache。デフォルトで有効になっているこのキャッシュ機構のせいで、ある PersistenceManagerFactory インスタンスから作成した PersistenceManager を用いて DB 上のデータを書き換えても、他の PersistenceManagerFactory インスタンスから作成した PersistenceManager からはその変更が見えない(=古いデータを参照し続ける)ということが起こる。この問題は、"datanucleus.cache.level2.type" 変数に none をセットすると回避できる。jdoconfig.xml に次のように追加することになる。
    <property name="datanucleus.cache.level2.type" value="none"/>
    
    コード上で変更する場合は次のとおり。
    pm.setProperty("datanucleus.cache.level2.type", "none");
    
  22. つづく

2014年2月23日日曜日

Apache Shiro Configuration (日本語訳)

# Apache Shiro Configuration の日本語訳

  単純なコマンドライン・アプリケーションからクラスター化された大規模エンタープライズアプリケーションに至るまで、あらゆる環境で動作するように Shiro は設計されています。環境は多岐にわたるため、適切に設定をおこなえるよう、設定方法も数多く存在します。この節では、Shiro core によりサポートされる設定方法について説明します。

多くの設定オプション

  Shiro の SecurityManager 実装群と補助コンポーネント群は、全て JavaBeans 互換です。これにより、事実上 Shiro は、通常の Java、XML (Spring, JBoss, Guice, etc), YAML, JSON, Groovy Builder マークアップ、等々、設定書式を問わずに設定が可能となっています。


プログラムによる設定

  SecurityManager を作成してアプリケーションで利用可能にするための絶対的に一番簡単な方法は、コード内で org.apache.shiro.mgt.DefaultSecurityManager を作成してつなぐ方法です。

例:
// Realm インスタンスを初期化するかどこかから取得する。
// Realm については後述します。
Realm realm = ...;

// SecurityManager のインスタンスを作成します。
SecurityManager securityManager = new DefaultSecurityManager(realm);

// static メモリーを介してアプリケーション全体から
// SecurityManager インスタンスを利用できるようにします。
SecurityUtils.setSecurityManager(securityManager);

  驚くべきことに、たった三行のコードで、多くのアプリケーションにとって十分に機能する Shiro 環境が用意できました。とても簡単だったでしょう!?


SecurityManager オブジェクトグラフ

  アーキテクチャーの章で説明したように、Shiro の SecurityManager 実装群はその本質として、セキュリティーに特化したコンポーネント群がネストした、モジュール式オブジェクトグラフです。コンポーネント群もまた JavaBeans 互換であるため、SecurityManager およびその内部オブジェクトグラフを設定するのに、ネストされたコンポーネント群の getter メソッドと setter メソッドを呼ぶことができます。

  例えば、セッション管理をカスタマイズするため、カスタム仕様の SessionDAO を使うように SecurityManager を設定したいのであれば、ネストしている SessionManagersetSessionDAO メソッドを用い、SessionDAO を直接セットすることができます。

...

// セキュリティーマネージャー
DefaultSecurityManager securityManager
   = new DefaultSecurityManager(realm);

// セッション DAO
SessionDAO sessionDAO
   = new CustomSessionDAO();

// セッション DAO を直接設定する。
((DefaultSessionManager)securityManager.getSessionManager())
    .setSessionDAO(sessionDAO);

...

  直接メソッドを起動することで、SecurityManager のオブジェクトグラフのあるゆる箇所を設定することができます。

  しかし、プログラムによるカスタマイズが単純だとしても、それが実世界のほとんどのアプリケーションにとって理想の設定方法というわけではありません。プログラムによる設定がアプリケーションにとって不適切となりうる理由が幾つかあります。

  • 実装に関する知識を有することと実装を初期化することが求められます。 具体的な実装に関することや、それがどこに存在するのかについて気にしなくてもいいのであれば、そちらのほうが良いでしょう。
  • Java の型安全性のため、get* メソッドで取得したオブジェクトを個別の実装へとキャストすることが求められます。 キャストが多いのは醜く、冗長であり、実装クラスに強く縛られてしまいます。
  • SecurityUtils.setSecurityManager メソッドでは、初期化された SecurityManager インスタンスを JVM の static なシングルトンにしてしまいます。多くのアプリケーションではそれでいいかもしれませんが、同一 JVM 上で複数の Shiro アプリケーションが実行されるようなことがあれば、問題が発生します。個々のアプリケーションの中でシングルトンではあるものの static なメモリー参照ではない、というのであれば、そちらのほうが良いでしょう。
  • Shiro の設定を変更するたびにアプリケーションの再コンパイルが求められます。

  とはいえ、このような欠点があるものの、直接プログラムで操作する方法は、例えばスマートフォンアプリケーションのようなメモリに制約のある環境では依然として価値があります。メモリに制約のある環境でアプリケーションを実行するのでなければ、テキストによる設定のほうが、使いやすく、また読みやすいでしょう。


INI 設定

  ほとんどのアプリケーションでは、ソースコードに影響を与えることなく変更できるテキストベースの設定により恩恵を受けることができます。また、Shiro の API に詳しくない人でも、理解しやすいという利点もあります。

  サードパーティーへの依存を最小限にとどめ、全ての環境で機能する公約数的なテキストベース設定メカニズムを確実に提供するため、Shiro は、SecurityManager オブジェクトグラフおよび補助コンポーネントを構築するための INI フォーマットをサポートしています。INI は読みやすく、設定しやすく、準備も単純で、ほとんどのアプリケーションに適しています。


INI から SecurityManager を生成する

  INI 設定に基づいて SecurityManager を構築する例を二つ挙げます。


INI リソースによる SecurityManager

  INI リソースパスから SecurityManager インスタンスを生成することができます。リソースは、file:, classpath:, url: というプレフィックスにより、ファイルシステム、クラスパス、URL から取得することができます。この例では、クラスパスのルートから shiro.ini を読み込んで SecurityManager インスタンスを得るためにファクトリークラスを使用しています。

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.IniSecurityManagerFactory;

...

// クラスパスのルートから shiro.ini を読み込む。
Factory<SecurityManager> factory
    = new IniSecurityManagerFactory("classpath:shiro.ini");

// ファクトリーから SecurityManager を構築する。
SecurityManager securityManager = factory.getInstance();

// SecurityManager をセットする。
SecurityUtils.setSecurityManager(securityManager);


INI インスタンスによる SecurityManager

  お望みであれば、org.apache.shiro.config.Ini クラスから INI 設定をプログラムにより構築することもできます。Ini クラスは JDK の java.util.Properties クラスと同じような動作をしますが、セクション名による分割をサポートしています。

例:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.Ini;
import org.apache.shiro.config.IniSecurityManagerFactory;

...

Ini ini = new Ini();

// 必要に応じて Ini インスタンスを設定する。
...

// Ini インスタンスからファクトリーインスタンスを作る。
Factory<SecurityManager> factory = new IniSecurityManagerFactory(ini);

// ファクトリーから SecurityManager を構築する。
SecurityManager securityManager = factory.getInstance();

// SecurityManager をセットする。
SecurityUtils.setSecurityManager(securityManager);

  INI 設定から SecurityManager を構築する方法はわかったので、Shiro INI 設定の定義方法をみていきましょう。


INI セクション

  INI は基本的に、一意の名前を持つセクション群で構成される、キー/値の組によるテキストベースの設定です。キーは各セクション内では一意ですが、設定全体で一意ではありません(この点は JDK Properties とは異なります)。とはいえ、各セクションは一つの Properties 定義のように見えます。

  コメント行はオクトソープ(# - ハッシュ、パウンド、ナンバーとも呼ばれます)もしくはセミコロンで始めます。

Shiro が解釈するセクションの例:
# =======================
# Shiro INI 設定
# =======================

[main]
# オブジェクトとそのプロパティーをここで定義します。
# securityManager や Realm、その他 SecurityManager を
# 構築するのに必要なもの。


[users]
# users セクションは、限られた数のユーザーアカウント群を
# 固定的に用意する必要があるときに使います。


[roles]
# roles セクションは、限られた数のロール群を固定的に
# 用意する必要があるときに使います。


[urls]
# urls セクションは、Web アプリケーションにおいて、
# URL ベースのセキュリティーのために使用します。



[main]

  [main] セクションは、アプリケーションの SecurityManager インスタンスおよびそれが依存するコンポーネント(Realm など)を設定する場所です。

  SecurityManager やそれが依存するコンポーネントといったオブジェクトインスタンスを INI で設定するのは、名前/値の組しか使えないことを考えると、難しそうに思えます。しかし、ちょっとした決まり事とオブジェクトグラフに関する理解があれば、かなり多くのことができることが分かるでしょう。Shiro では、単純ではあるけれども極めて簡潔な設定メカニズムを可能とするため、このような想定をしています。

  我々はよく、このアプローチを「貧者の」依存性注入 ("poor man's" Dependency Injection) と好んで呼んでいます。Spring, Guice, JBoss XML の本格的な依存性注入メカニズムほど強力ではないものの、複雑さを排しながらも多くのことが実現できることにあなたも気付くことでしょう。もちろん他の設定方法も利用可能ですが、Shiro を使う上では必要とされません。

  あなたの欲求を刺激するため、有効な [main] 設定の例を挙げます。詳細はこれから説明していきますが、「直感だけでも行われていることの大半を理解できる」と感じるのではないでしょうか。

[main]
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher

myRealm = com.company.security.shiro.DatabaseRealm
myRealm.connectionTimeout = 30000
myRealm.username = jsmith
myRealm.password = secret
myRealm.credentialsMatcher = $sha256Matcher

securityManager.sessionManager.globalSessionTimeout = 1800000


オブジェクトを定義する

  [main] セクションの次の箇所を見てみましょう。

[main]
myRealm = com.company.security.shiro.DatabaseRealm

  この行では com.company.shiro.realm.MyRealm という型の新しいオブジェクトインスタンスを生成し、myRealm という名前を付けて以降の設定で参照できるようにしています。

  インスタンス化されたオブジェクトが org.apache.shiro.util.Nameable インターフェースを実装している場合、Nameable.setName メソッドが名前の(この例では myRealm)を引数として実行されます。


オブジェクトのプロパティーを設定する

<プリミティブ値>

  単純なプリミティブプロパティーは、等号記号を使うだけで設定できます。

...
myRealm.connectionTimeout = 30000
myRealm.username = jsmith
...

  これらの設定行は次のようなメソッド呼び出しへと変換されます。

...
myRealm.setConnectionTimeout(30000);
myRealm.setUsername("jsmith");
...

  どうしてこんなことが可能なのかですかって? 全てのオブジェクトが Java Beans 互換の POJO であることを前提としているからです。

  Shiro の内部実装は、デフォルトでは、プロパティーをセットするときの大変な処理を全て Apache Commons BeanUtils を使っておこなっています。そのため、INI 値はテキストですが、BeanUtils は文字列値を適切なプリミティブ型に変換する方法と、対応する JavaBeans の setter メソッドを呼び出す方法を知っているのです。


<参照値>

  セットする値がプリミティブではない、何らかのオブジェクトの場合はどうなるでしょうか? はい、その場合はドル記号($)を使い、定義済みのインスタンスを参照してください。

例:
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
myRealm.credentialsMatcher = $sha256Matcher
...

  これは単純に sha256Matcher という名前で定義されているオブジェクトを探し、BeanUtils を使って myRealm インスタンスにそのオブジェクトを(myRealm.setCredentialsMatcher(sha256Matcher) メソッドを呼んで)セットします。


<ネストしたプロパティー>

  INI 行の等号記号の左側でドット記法を使い、オブジェクトグラフをたどって最終的なオブジェクト/プロパティーを取得することができます。例えば、この設定行は

...
securityManager.sessionManager.globalSessionTimeout = 1800000
...

(BeanUtils により)下記のように変換されます。

securityManager.getSessionManager().setGlobalSessionTimeout(1800000);

  グラフは必要なだけ深くたどっていくことができます。

object.property1.property2....propertyN.value = blah

BeanUtils プロパティーサポート

BeanUtils.setProperty メソッドがサポートするプロパティー代入操作は、set/list/map 要素の代入を含め、全て Shiro の [main] セクションで機能します。より詳しい情報は Apache Commons BeanUtils のウェブサイトとドキュメントを参照してください。


<バイト配列値>

  バイト配列をそのままテキストフォーマットで指定することはできないので、バイト配列をテキストに符号化しなければなりません。値は、Base64(デフォルト)もしくは十六進数で符号化した文字列として指定することができます。Base64 がデフォルトなのは、Base64 による符号化が値を表現するのにより少ないテキストで済むからです。符号化に使用するアルファベット集合がより多く、つまり、トークンが短くなるのです(テキストによる設定にとってはちょっと好都合です)。

# cipherKey 属性はバイト配列。デフォルトでは、バイト配列属性に
# 対応するテキストは Base64 で符号化されているものと想定されます。

securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==
...

  しかし、かわりに十六進数による符号化を使いたいというのであれば、文字列トークンの先頭に 0x (「ゼロ」と「x」)を付けてください:

securityManager.rememberMeManager.cipherKey = 0x3707344A4093822299F31D008


<集合プロパティー>

  List, Set, Map も他のプロパティーと同様、直接またはネストしたプロパティーとしてセットすることができます。SetList は、オブジェクトの値をカンマで区切りで並べます。

SessionListener を複数指定する例:
sessionListener1 = com.company.my.SessionListenerImplementation
...
sessionListener2 = com.company.my.other.SessionListenerImplementation
...
securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2

  Map については、キーと値をコロンで連結した組をカンマ区切りで並べます。

object1 = com.company.some.Class
object2 = com.company.another.Class
...
anObject = some.class.with.a.Map.property

anObject.mapProperty = key1:$object1, key2:$object2

  上記の例では、$object1 で参照されるオブジェクトが String のキー key1map に格納されます。つまり、map.get("key1")object1 を返します。他のオブジェクトをキーとして使うこともできます。

anObject.map = $objectKey1:$objectValue1, $objectKey2:$objectValue2
...


留意事項

<順序が意味を持つ>

  ここまでに述べた INI フォーマットとその決まり事は、とても使い勝手が良く、理解も容易です。しかし、他の XML ベースの仕組みほど強力ではありません。上記の仕組みを使う上で、理解しなければならない最も重要なことは、順序が意味を持つ、ということです!

慎重に

  オブジェクトの生成と値の代入は、[main] セクションに現れた順番で実行されます。各行は最終的に JavaBeans の getter/setter メソッドに変換され、同じ順序で実行されます! 設定を書くときにはこの点を覚えておいてください。


<インスタンスの上書き>

  どのオブジェクトも、設定内で後から定義された新しいインスタンスで上書きすることができます。そのため、例えば二番目の myRealm の定義は一番目のものを上書きします。

...
myRealm = com.company.security.MyRealm
...
myRealm = com.company.security.DatabaseRealm
...

  これにより、myRealmcom.company.security.DatabaseRealm のインスタンスとなり、以前のインスタンスが使われることはありません(ガベージコレクションの対象となります)。


<デフォルト SecurityManager>

  ここまでの完全な例の中で SecurityManager インスタンスのクラスが定義されておらず、ネストしたプロパティーをいきなり設定していたことに気が付かれたかもしれません:

myRealm = ...

securityManager.sessionManager.globalSessionTimeout = 1800000
...

  これは、securityManager インスタンスが特別なものだからです。既に生成済みで準備ができており、どの SecurityManager 実装クラスをインスタンス化すべきかを設定者が知る必要はありません。

  もちろん、独自の実装を実際に指定したい場合には、そうすることができます。「<インスタンスの上書き>」で説明したように、独自の実装を定義してください:

...
securityManager = com.company.security.shiro.MyCustomSecurityManager
...

  もちろん、この手順が必要となることは滅多にないでしょう。Shiro の SecurityManager の実装はカスタマイズ性が高く、通常必要となるものは設定可能だからです。本当に必要かどうか、自分自身に(またはユーザーの方々に)尋ねてみたいと思うかもしれません。


[users]

  [users] セクションでは、ユーザーアカウントを固定的に定義することができます。ユーザーアカウントの数が限られる環境や、ユーザーアカウントを実行時に動的に生成する必要のない環境では、大抵便利に使うことができます。

[users]
admin = secret
lonestarr = vespa, goodguy, schwartz
darkhelmet = ludicrousspeed, badguy, schwartz

自動的な IniRealm

  空ではない [users] セクションもしくは [roles] セクションを定義すると、自動的に org.apache.shiro.realm.text.IniRealm のインスタンスが生成され、[main] セクション内で iniRealm という名前で参照することができます。これまで説明した方法と同じように、当該インスタンスを設定するこが可能です。


行フォーマット

  [users] セクションの各行は次の書式に従っていなければなりません。

ユーザー名 = パスワード, ロール名1, ロール名2, ..., ロール名N
  • 等号記号の左側の値はユーザー名です。
  • 等号記号の右側の最初の値はユーザーのパスワードです。パスワードの指定は必須です。
  • パスワードに続くカンマ区切りの値は、ユーザーに割り当てるロール名です。ロール名の指定は任意です。

パスワードの暗号化

  [users] セクションのパスワードを平文にしたくないのであれば、好きなハッシュアルゴリズム(MD5, SHA-1, SHA-256, 等)を使って暗号化し、結果として得られた文字列をパスワードとして使うことができます。デフォルトでは、パスワード文字列は十六進数で符号化されているものと想定されますが、かわりに Base64 での符号化を用いるように設定することもできます(以下に説明します)。

簡単で安全パスワード

  無駄な時間をかけずにベストプラクティスを実践するため、パスワードおよび他のどんなタイプのリソースのハッシュ値でも計算することができる Shiro のコマンドラインハッシュ計算プログラムを使いたいと思うかもしれません。INI [users] パスワードの暗号化をおこなうには特に便利です。

  ハッシュ化したパスワードをテキストで設定したあとは、暗号化されていることを Shiro に伝えなければなりません。これは、[main] セクションで暗黙的に生成された iniRealm が、あなたの使用したハッシュアルゴリズムに対応する適切な CredentialsMatcher の実装を使うように設定することでおこないます。

[main]
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
iniRealm.credentialsMatcher = $sha256Matcher
...

[users]
# user1 = SHA-256 でハッシュ化したパスワードの十六進数表現, role1, role2, ...
user1 = 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b, role1, role2, ... 

  他のオブジェクトと同様、使用するハッシュストラテジーに合わせて、CredentialsMatcher を設定することができます。例えば、ソルト処理を使用するかどうかや、ハッシュ処理を何回繰り返すか、ということを設定できます。これらの設定項目があなたにとって有用であるならば、ハッシュストラテジーをより深く理解するため、org.apache.shiro.authc.credential.HashedCredentialsMatcher の JavaDoc を参照してください。

  例えば、users のパスワード文字列をデフォルトの十六進数表記ではなく Base64 表記にするのであれば、次のように指定します。

[main]
...
# true = 十六進数, false = Base64
sha256Matcher.storedCredentialsHexEncoded = false


[roles]

  [roles] セクションでは、[users] セクションで定義したロールにパーミッションを関連付けます。もう一度言いますが、これは、ロールの数が限られているか、ロールを実行時に動的に生成する必要がない環境において便利です。例を挙げます。

[roles]
# admin ロールは全てのパーミッションを持ちます。
# ワイルドカード '*' が全てのパーミッションを意味します。
admin = *

# schwartz ロールは lightsaber については何でもできます。
schwartz = lightsaber:*

# goodguy ロールは、eagle5 というナンバープレート
# (インスタンス固有の ID) を持つ winnebago (型) を
# drive (アクション) することを許可されています。
goodguy = winnebago:drive:eagle5


行フォーマット

  [roles] セクションの各行は、ロールからパーミッション群への対応を、次のような形式で定義しなければなりません。

ロール名 = パーミッション定義1, パーミッション定義2, ..., パーミッション定義N

  ここで「パーミッション定義」は任意の文字列ですが、ほとんどの人は、使いやすさと柔軟性のため、org.apache.shiro.authz.permission.WildcardPermission の書式に従う文字列を使おうと考えます。パーミッションに関する詳細情報と、それがどのように役に立つのかについては、パーミッションのドキュメントを参照してください。

内部カンマ

  もしも個々のパーミッション定義内でカンマ区切りを用いたい場合(例えば printer:5thFloor:print,info など)、パースエラーを避けるため、定義をダブルクォーテーションで囲む必要があります。

"printer:5thFloor:print,info"

パーミッションの無いロール

  パーミッションに関連付ける必要のないロールは、そうしたくないのであれば [roles] セクションに書く必要はありません。まだ存在しないロールを生成するだけであれば、[users] セクションにロール名を定義するだけで十分です。


[urls]

  このセクションとそのオプションについては Web に関する章で説明します。


文書のご協力をお願いします

  本文書が Apache Shiro を扱う作業の手助けとなることを望みつつ、コミュニティーでは常に文書の改良と拡張をおこなっています。Shiro プロジェクトを手伝ってくださるのであれば、必要だと思われた箇所から文書の訂正、拡張、追加のご検討をお願いします。どんなに小さなことであっても、あなたの貢献は全て、コミュニティーの発展、そして Shiro の改善につながります。

  文書による貢献の最もやり易い方法は、ユーザーフォーラムもしくはユーザーメーリングリストに文書を送付いただくことです。