書籍「詳解Tomcat」を読んで
本書のレビューアの方から頂いたので読んでみました。以下、感想です。
- 作者: 藤野圭一
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/12/26
- メディア: 大型本
- この商品を含むブログを見る
最近、Twitterのタイムラインで『はじめてのXXXや、XXX 入門じゃなくて、もっと至高のXXXとか終焉のXXXみたいな書籍が欲しい』のような意見を目にした気がしますが、本書はまさに詳解Tomcatでした。設定方法を羅列するのではなく、Tomcatはどういくアーキテクチャで成り立っていて、各機能の詳細な挙動はどのような仕様なのかがまとめられています。
1章 Tomcatとは 〜 2章 Tomcatの基本 までは、背景やインストール方法などの色々な書籍で見かける導入部分の内容です。3章 アーキテクチャからが本番です。
Tomcatアーキテクチャの文書化が嬉しい
普段Tomcatを使っている人であれば、catalina/coyoteといったキーワードはスタックトレースやログで一度は目にした方も多いと思います。本書では、Servletコンテナ実装であるCatalinaアーキテクチャ(3章)およびHTTPコネクタ実装であるCoyote(5章)の全体像が文書化されています。
server.xmlを書いていると、<Service>、<Connector>、<Engine>などのCatalinaを構成するコンポーネント名が出てきますが、お恥ずかしながら今まで私はコンフィグ例を参照しながらなんとなく設定していたので、各コンポーネントの役割が理解できていませんでした。
トラブル対応時、いざTomcatのソースを読む時にアーキテクチャを理解していることはすごく重要だと思います。自身を振り返ると『Tomcat7以前ではなぜリクエスト処理後もスレッドがプールに返らないのか?』、『maxThreadsを超えるリクエストが来た時の仕様はどうなってるの?』などの問い合わせにもっと早く対応できたなと。
Tomcat固有機能の解説が嬉しい
5章 応用機能 以降ではTomcat固有の様々な便利機能の使い方、各機能がどういう仕様なのかが詳解されています。以下のような機能の詳細な解説は他の書籍ではあまり見かけないと思います。
Apache ShiroでBCryptを使う
先日のJava EE Advant Calander 2014にApache Shiroを使ってみましたを書いたところ、Shiroのデフォルトのハッシュポリシーソルト付きSHA-256, 500000 イテレーション
について、ハッシュイテレーションのアルゴリズムをShiroのように独自実装せずに、総当たり攻撃耐性を持たせるために低速化させた既存のアルゴリズム(PBKDF2, BCrypt)もあるよ*1*2*3*4 と教えてもらったため、Shiroに組み込めないか試してみる。
ハッシュアルゴリズムをどうするか
@watarinさんのJavaでパスワードをハッシュ化するのに良い方法を調べてみたを入り口に色々と調べてみたが、以下の理由でBCrypt (Java実装はjBCrypt) を選んでみた。
- Java7で動かしたい (PBKDF2WithHMAC-SHA-256がJava8から)
- JBossEAP6.3が今のところJava8をサポートしてないため
- ここで言及されている意見
- しかし、実検証データがないのはちょっと不安
- 処理において配列アクセスの多いBCryptがGPU対策に本当によいのかは不明
- Java実装jBCryptはSpringSecurityやPicketlinkに若干の修正の上、引用されている
実装する
公式コミュニティにおいても、SHIRO-290 Create a BCrypt Hash implementationとしてBCryptサポートが要望されているが、添付されている.patch案がストレッチ強度が調整できないようになってたので、作ってみる。
BCrypt向けPasswordServiceを作る
Shiro1.2より、パスワードのハッシュ化と照合を担うPasswordServiceインタフェースが用意されているため、デフォルト実装であるDefaultPasswordServiceクラスのコードを参考にして作ってみる。
public class BCryptPasswordService implements PasswordService { public static final int DEFAULT_BCRYPT_ROUND = 10; private int logRounds; public BCryptPasswordService() { this.logRounds = DEFAULT_BCRYPT_ROUND; } public BCryptPasswordService(int logRounds) { this.logRounds = logRounds; } @Override public String encryptPassword(Object plaintextPassword) { if (plaintextPassword instanceof String) { String password = (String) plaintextPassword; return BCrypt.hashpw(password, BCrypt.gensalt(logRounds)); } throw new IllegalArgumentException( "BCryptPasswordService encryptPassword only support java.lang.String credential."); } @Override public boolean passwordsMatch(Object submittedPlaintext, String encrypted) { if (submittedPlaintext instanceof char[]) { String password = String.valueOf((char[]) submittedPlaintext); return BCrypt.checkpw(password, encrypted); } throw new IllegalArgumentException( "BCryptPasswordService passwordsMatch only support char[] credential."); } public void setLogRounds(int logRounds) { this.logRounds = logRounds; } public int getLogRounds() { return logRounds; } }
ハッシュパスワード生成コードの修正
アカウント生成時にDefaultPasswordServiceを使っている部分を今作ったBCryptPasswordServiceに置き換える。
// PasswordService ps = new DefaultPasswordService(); // ラウンド数をデフォルトから変えたい場合はコンストラクタに設定 // デフォルトは10 // PasswordService ps = new BCryptPasswordService(12); PasswordService ps = new BCryptPasswordService(); String encryptedPassword = ps.encryptPassword(form.getPassword());
コメントアウトしているように、ストレッチ強度を返る時にはコンストラクタに設定する。BCryptの特徴として、引数のラウンド数は2のXX乗のような指数で示すため、デフォルトの10を20に変更したら、計算量は2倍ではなく1024倍になるので注意。
ストレッチ強度はマシンスペックおよび許容できる遅延に応じて調整する。
shiro.iniの修正
パスワード照合時に先ほど作ったBCryptPasswordServiceが使われるように、shiro.iniのpasswordMatcherを修正する。
[main] # Default SHA-256, 500000 hash iteration, use salt #passwordService = org.apache.shiro.authc.credential.DefaultPasswordService # BCrypt PasswordService passwordService = net.agetsuma.sample.shiro.security.BCryptPasswordService passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher passwordMatcher.passwordService = $passwordService
ハッシュパスワード(例: $2a$10$e4l5...)にイテレーションのラウンド数(例では10)が組み込まれているため、shiro.iniで明示的にラウンド数を設定しなくてもパスワード照合が可能。
試しに作ってみたものの一式はGithubに置いてあります。
まとめ
*1:エフセキュアブログ : 再録:パスワードは本当にSHA-1+saltで十分だと思いますか?
*3:ユーザーのパスワードを安全に保管する方法について - 11月 - 2013 - Sophos Press Releases, Security News and Press Coverage - Sophos Press Office | Sophos News and Press Releases - ソフォス
*4:6.4. パスワードハッシュ化 — TERASOLUNA Global Framework Development Guideline 1.0.0.publicreview documentation
GlassFish4.1をなおしてみた
この記事は GlassFish Advent Calendar 2014の12/25分の記事です。昨日は
@backpaper0さんのGlassFishでセッションIDを生成してるところでした。
2013/11にGlassFishの商用サポートの終了がアナウンスされて私も不安に思っていましたが、冷静に考えるとGlassFishはそもそもオープンソース(CDDL/GPLv2)です。ソースが公開されているので、もし不具合や納得いかない動作があれば自分で修正することができます。
先日、Java EE Advant Calanderの12/17分の記事を書いていたときに既知の不具合を踏んだので試しになおしてみました。以下の修正案は、私がGlassFishのソースに適当に試行錯誤して修正したとても怪しいものなので、間違ってたらすいません。
@Transactionalの例外時に原因がセットされていない
私が踏んだ不具合はGLASSFISH-21172として既にJIRAに登録されている、Java EE7から導入された@Transactionalでトランザクション境界を設定した場合、トランザクション境界内からランタイム例外が投げられても原因がよくわからないものです。
2014/12/25現在においても、今のところ修正はされていません。
具体的には以下のようなコードを実行すると問題に当たります。
@Dependent public class OrderService { @Inject OrderRepository repository; @Transactional public Order takeOrder(Order order) { return repository.persist(order); } } @Dependent public class OrderRepository { @PersistenceContext private EntityManager em; public Order persist(Order order) { Order o = em.merge(order); throw new NullPointerException("ぬるぽ"); } }
RepositoryからNPEを投げてます。上記のコードを動かし、catchせずに無視すると、GlassFish4.1では以下のようにログ出力されます。
[2014-12-16T22:04:57.576+0900] [glassfish 4.1] [SEVERE] [] [org.glassfish.jersey.server.ServerRuntime$Responder] [tid: _ThreadID=30 _ThreadName=http-listener-1(5)] [timeMillis: 1418735097576] [levelValue : 1000] [[ An exception has been thrown from an exception mapper class org.glassfish.jersey.gf.cdi.internal.TransactionalExceptionMapper$Proxy$_$$_WeldClientProxy. javax.transaction.TransactionalException: Managed bean with Transactional annotation and TxType of REQUIRED encountered exception during commit javax.transaction.RollbackException: Transaction marked for rollback. at org.glassfish.cdi.transaction.TransactionalInterceptorRequired.transactional(TransactionalInterceptorRequired.java:105) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at org.jboss.weld.interceptor.reader.SimpleInterceptorInvocation$SimpleMethodInvocation.invoke(SimpleInterceptorInvocation.java:74) at org.jboss.weld.interceptor.chain.AbstractInterceptionChain.invokeNext(AbstractInterceptionChain.java:116) at org.jboss.weld.interceptor.chain.AbstractInterceptionChain.invokeNextInterceptor(AbstractInterceptionChain.java:94) at org.jboss.weld.interceptor.proxy.InterceptorMethodHandler.executeInterception(InterceptorMethodHandler.java:43) at org.jboss.weld.interceptor.proxy.InterceptorMethodHandler.invoke(InterceptorMethodHandler.java:36) at org.jboss.weld.bean.proxy.CombinedInterceptorAndDecoratorStackMethodHandler.invoke(CombinedInterceptorAndDecoratorStackMethodHandler.java:51) at net.agetsuma.transactional.OrderService$Proxy$_$$_WeldSubclass.takeOrder(Unknown Source) .. 途中省略 Caused by: javax.transaction.RollbackException: Transaction marked for rollback. at com.sun.enterprise.transaction.JavaEETransactionImpl.commit(JavaEETransactionImpl.java:445) at com.sun.enterprise.transaction.JavaEETransactionManagerSimplified.commit(JavaEETransactionManagerSimplified.java:854) at com.sun.enterprise.transaction.TransactionManagerHelper.commit(TransactionManagerHelper.java:81) at org.glassfish.cdi.transaction.TransactionalInterceptorRequired.transactional(TransactionalInterceptorRequired.java:97) ... 66 more
なんとなくロールバックしたことはわかりますが、NPEが投げられたことが原因とはわかりません。システムの保守をやっていると、『環境的な要因なのか』『入力が間違ってたのか』などの例外の原因がわからないと結構困ります。
なおしてみる
オープンソースなので、困ったらなおしてみます。バグ報告やパッチ投稿方法、ロギングの指針などコミュニティに関するルールはGlassFish Project - Community Rulesにまとまっています。
ソースのダウンロード
GlassFishのソースコードはSubversionで管理されています。とりあえずソースを確認したいだけなので、4.1タグのソースをsvn export
で持ってきます。
svn export https://svn.java.net/svn/glassfish~svn/tags/4.1
ソースコードを読む
例外のスタックトレースを改めて見ると、JTA実装であるJavaEETransactionImpl.javaの445行目において、commitを試みたが既にトランザクションがロールバック確定とマーキングされていたので、RollbackExceptionを投げたとあります。
Caused by: javax.transaction.RollbackException: Transaction marked for rollback. at com.sun.enterprise.transaction.JavaEETransactionImpl.commit(JavaEETransactionImpl.java:445)
JavaEETransactionImpl.commitメソッドを見てみます。
com.sun.enterprise.transaction.JavaEETransactionImpl.commit()メソッド (445行目付近)
439 if ( isRollbackOnly() ) { 440 // rollback nonXA resource 441 if ( nonXAResource != null ) 442 nonXAResource.getXAResource().rollback(xid); 443 444 localTxStatus = Status.STATUS_ROLLEDBACK; 445 throw new RollbackException(sm.getString("enterprise_distributedtx.mark_rollback")); 446 }
JavaEETransactionalImpl.commit()の445行目で、commit()実行時に既にjavax.transaction.Transaction.setTransactionRollback()
メソッドの実行によって、ロールバックすべきとマーキングされていた場合、ロールバックの上、さらにRollbackException
がスローされています。
このjavax.transaction.RollbackExceptionのjavadocを見ると、RollbackExceptionには例外causeを引数に取るコンストラクタは定義されていないことに気づきます。例外スタックトレースにcauseが入ってこないのは、ここのコードが原因のようです。しかし、例外の原因を設定するコンストラクタがなければ、この部分を修正して対処することは難しいと思いました。
次に、そもそもランタイム例外であるNPEが投げられてロールバックが確定しているのに、何故コミットを実行しようとしているのかが気になりました。あえてJavaEETransactionalImpl.commit()
を実行によってロールバックさせずに、最初からロールバックを実行すれば良いのではと。
そこで、コミットを呼び出しているコードを探してみます。
org.glassfish.cdi.transaction.TransactionalInterceptorRequired.transactionalメソッド
69 @AroundInvoke 70 public Object transactional(InvocationContext ctx) throws Exception { ... 途中省略 91 Object proceed = null; 92 try { 93 proceed = proceed(ctx); 94 } finally { 95 if (isTransactionStarted) 96 try { 97 getTransactionManager().commit(); 98 } catch (Exception exception) { 99 String messageString = 100 "Managed bean with Transactional annotation and TxType of REQUIRED " + 101 "encountered exception during commit " + 102 exception; 103 _logger.log(java.util.logging.Level.INFO, 104 CDI_JTA_MBREQUIREDCT, exception); 105 throw new TransactionalException(messageString, exception); 106 } 107 }
proceed(ctx)の中から例外が投げられても、問答無用でcommit()しています。
proceedメソッドの挙動が気になります。このメソッドは今確認したTransactionalInterceptorRequiredクラスの親クラスTransactionalInterceptorBase
クラスで定義されています。
org.glassfish.cdi.transaction.TransactionalInterceptorBase クラス
208 public Object proceed(InvocationContext ctx) throws Exception { ... 途中省略 224 Object object; 225 try { 226 object = ctx.proceed(); 227 } catch (RuntimeException runtimeException) { 228 Class dontRollbackOnClass = 229 getClassInArrayClosestToClassOrNull(dontRollbackOn, runtimeException.getClass()); 230 if (dontRollbackOnClass == null) { //proceed as default... 231 markRollbackIfActiveTransaction(); 232 throw runtimeException; 233 }
トランザクション境界内の処理からRuntimeExceptionが投げられた場合、@Transactional(dontRollbackOn=XXX.class)に設定されているロールバック対象外の例外でなければ、231行目でトランザクションをロールバック対象とマークし、次の行でメソッドの中で発生した例外を再スローしています。
すなわち、現在の実装は例外が発生した時点ではロールバック対象としてマーキングして、例外が再スローされていますが、すぐその後に子クラスのTransactionalInterceptorRequiredクラスでcommit()が実行されているため、既にロールバック対象としてマーキング済によるコミット時例外によって、元の例外が上書きされていることによって、例外の原因がわからない事象が発生しているようです。
TransactionalInterceptorRequiredクラスの修正アイディア
@AroundInvoke public Object transactional(InvocationContext ctx) throws Exception { ... 途中省略 Object proceed = null; try { proceed = proceed(ctx); } finally { if (isTransactionStarted) { if (ロールバックマーク済み?) { getTransactionmanager().rollback(); } else { try { getTransactionManager().commit();
上記のように、ロールバック対象としてマーキングされている場合は、コミットせずに最初からロールバックして、投げられた例外がコミット例外によって上書きされないように修正したいです。
スペック(JTA1.2)の確認
GlassFish4.1はJava EE7仕様を実装した参照実装です。コードを修正するには仕様に違反しないか注意が必要です。
今回の場合、今まではロールバック時にはRollbackExceptionが返されていましたが、修正後にはアプリから投げられたランタイム例外をそのまま投げることになります。トランザクション周りの仕様はJava Transaction API (JTA)で決まれている為、JCPのサイトから仕様のPDFを入手して読んでみます。
JTA1.2仕様をRollbackExceptionで検索しても、『ロールバック時には必ずRollbackExceptionを投げること』のような記述は見当たらないため、アプリのランタイム例外をそのまま@Transactionalを付与したメソッドの呼び出し元に返しても大丈夫そうです。
不安なのでWildFly8.2でも試してみますが、@Transactionalの中からランタイム例外を投げると、特にラップされずに返ってきます。
22:37:46,952 ERROR [io.undertow.request] (default task-2) UT005023: Exception handling request to /TransactionalTest-1.0-SNAPSHOT/rest/orders: org.jboss.resteasy.spi.UnhandledException: java.lang.NullPointerException: ぬるぽ at org.jboss.resteasy.core.ExceptionHandler.handleApplicationException(ExceptionHandler.java:76) [resteasy-jaxrs-3.0.10.Final.jar:] ... 途中省略 Caused by: java.lang.NullPointerException: ぬるぽ at net.agetsuma.transactional.OrderRepository.persist(OrderRepository.java:24) [classes:] at net.agetsuma.transactional.OrderService.takeOrder(OrderService.java:26) [classes:]
ソースコード修正
最終的になおしてみたコードの.patchはGistにおいています。
patchコマンドで適用できます。
patch -p1 -d 4.1 < GLASSFISH-21172.patch
修正したソースコードをGlassFish本流にパッチとして盛り込みをお願いする場合は、Oracle Contributor License (OCA)へのサインとオラクル社への送付が必要です。
修正したモジュールのビルド
GlassFish全体ビルドには時間が掛かるので、修正したモジュールのみビルドします。今回の対象モジュールはweld-integration
です。
cd 4.1/appserver/web/weld-integration mvn package
GlassFish4.1はJDK7u9以上ででビルドします。JDK8でビルドすると、以下のようなメッセージが出力されて失敗します。
[WARNING] Rule 0: org.apache.maven.plugins.enforcer.RequireJavaVersion failed with message: You need JDK greater or equal than 1.7.0-09 (JDK8 not supported yet)
モジュールの差し替え
新たに生成した weld-integration.jar を、元のファイルから差し替えます。
cp target/weld-integration.jar /home/test/work/glassfish4/glassfish/modules/weld-integration.jar
統合テストの実行
GlassFishのソースにはQuicklook Testsと呼ばれる統合テストが含まれています。修正によって新たにバグを盛り込んでいないか確認したいので動かしてみます。
GlassFish Project - Community Rulesでは、maven runtest
で動かせると書いてあります(mvnのこと?)。陳腐化しているようです。
試しに、普通にtestフェーズを指定して動かしてみます。テスト対象のGlassFishホームも合わせて設定してみます。
cd glassfish4/4.1/appserver/tests/quicklook mvn -Dglassfish.home=/home/test/work/glassfish4/glassfish test ... testng-summary: [echo] [testng] [echo] [testng] =============================================== [echo] [testng] QuickLookTests [echo] [testng] Total tests run: 120, Failures: 0, Skips: 0 [echo] [testng] =============================================== [echo] [testng]
これで良いのか自信がないですが、テストをパスしたようです。
なおした結果
ロールバックの原因となった例外がそのままスローされるので、ロールバック原因がすぐわかるようになりました。
[2014-12-23T14:35:25.032+0900] [glassfish 4.1] [WARNING] [] [javax.enterprise.web] [tid: _ThreadID=28 _ThreadName=http-listener-1(2)] [timeMillis: 1419312925032] [levelValue: 900] [[ StandardWrapperValve[org.netbeans.rest.application.config.ApplicationConfig]: Servlet.service() for servlet org.netbeans.rest.application.config.ApplicationConfig threw exception java.lang.NullPointerException: ぬるぽ at net.agetsuma.transactional.OrderRepository.persist(OrderRepository.java:24) at net.agetsuma.transactional.OrderService.takeOrder(OrderService.java:26) ... 省略
Apache Shiro を使ってみました
この記事は Java EE Advent Calendar 2014の12/17分の記事です。昨日は
@glory_ofさんのJAX-RSのレスポンスでした。明日は@nagaseyasuhitoさんです。
Java EE8では、『使い方が複雑・各APサーバ固有のレルム設定がよくわからん』とあまり評判のよくないセキュリティ周りの機能の再整理*1が行われようとしています。
しかし、Java EE8の仕様がリリースされるのはだいぶ先の2016年。
そんなに待てないので、Apache Shiroを試してみました。
Apache Shiroの既存の他の記事は部分的なコードの抜粋が多く、動かせるコードがあまり見当たらなかったので、GitHubにサンプルコードとしてまとめてみました。
Apache Shiro とは
Easy To Useを一番の目的にしたJavaのセキュリティフレームワークです。
歴史は古く2003年頃にJSecurityプロジェクトとして始まり、2008年よりApache Software Foundationに移管されました。Shiroは以下のような機能を持ちます。
- 認証 (Authentication)
- いわゆる『ログイン』機能です。
- 認可 (Authorization)
- アクセス制御。例えば、偉い人だけが人事計画情報が見れるなど。
- 暗号 (Cryptography)
- セッション管理
Apache Shiroを使うと、APサーバごとにレルム設定に悩まなくても同じ設定で認証・認可周りが実装できます。
アーキテクチャと設定
コード例とshiroのコンフィグであるshiro.iniを踏まえて紹介します。
例では、DBMSに認証・認可情報を保存していますが、LDAP連携もできます。
Subject
ユーザは、このSubjectと呼ばれるインタフェースを通して、認証・認可の判断をShiroに委ねます。Subjectには、以下のようにSecurityUtilsからアクセス可能です。
// Shiroによる認証コード Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(email, password); try { currentUser.login(token); } catch (AuthenticationException ae) { // authentication failed. }
SecurityManager
認証・認可に具体的にどのような仕組みを使うのかを管理しているクラスです。Shiroの設定ファイルであるshiro.iniをクラスパスからロードして、このSecurityManagerを組み立てます。実際のshoro.iniの例を示します。
[main] # Default SHA-256, 500000 hash iteration, use salt passwordService = org.apache.shiro.authc.credential.DefaultPasswordService passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher passwordMatcher.passwordService = $passwordService ds = org.apache.shiro.jndi.JndiObjectFactory ds.requiredType = javax.sql.DataSource ds.resourceName = java:comp/DefaultDataSource jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm jdbcRealm.authenticationQuery = SELECT password FROM useraccount WHERE email = ? jdbcRealm.userRolesQuery = SELECT userrole FROM useraccount WHERE email = ? jdbcRealm.credentialsMatcher = $passwordMatcher jdbcRealm.dataSource = $ds securityManager.realms = $jdbcRealm
PasswordMatcherの設定
- 最初の3行(passwordMatcher)は、パスワードの照合の仕組みを設定
- Shiroに組み込まれているDefaultPasswordServiceを指定
- ソルト付き/SHA-256/イテレーション50万回
データソース設定
- 次の3行(ds)は、jdbcRealmが使うデータベースのアクセス先設定
- コネクションプールを使いたいのでデータソース
java:comp/DefaultDataSource
を設定
- コネクションプールを使いたいのでデータソース
JDBCレルム設定
- 次の5行(jdbcRealm)は、認証・認可時にDBMSよりユーザ情報を取得するJdbcRealmを設定
- デフォルトの設定はソースコードより確認できます
authenticationQuery
は、認証時にパスワードを取得するSQL- デフォルト
select password from users where username = ?
- デフォルト
userRolesQuery
は、認可時にロール情報を取得するSQL。- デフォルト
select role_name from user_roles where username = ?
- デフォルト
jdbcRealm.credentialsMatcher
に先ほど設定したpasswordMatcherを指定- 認証時に平文パスワードをShiroに渡すと、PasswordMatcherの設定に基づきハッシュ化してDBMS上に保存しているハッシュパスワードと照合される
dataSource
に先ほど設定ししたdsを指定- SQL実行時にデータソースが使われる
SecurityManager設定
- 最後の
securityManager.realms = $jdbcRealm
で、セキュリティマネージャにレルムとして今まで設定してきたjdbcRealmを使うと設定
使い方
サンプルコードに実例が色々と含まれていますが、簡単にまとめます。
pom.xml
shiro-coreが本体で、shiro-webがカスタムJSPタグや、Servletフィルタの実装が入っています。WebアプリケーションでShiroを使う場合は両方とも必要です。
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.3</version> </dependency>
設定ファイル (shiro.ini)
内容については前述の通りです。
Webアプリケーション(.war)の場合、WEB-INF
の直下、またはクラスパスのルートmain/src/resources/shiro.ini
に置きます。
API
パスワードのハッシュ化
Shiroは『ユーザ作成機能』を持っていません。
その代わり、ユーザ作成に便利なパスワードハッシュ機能を持っています。
Shiroが用意するDefaultPasswordServiceクラスではを使うと、ソルト付き/SHA-256/イテレーション50万回のパスワードハッシュが2行のコードで取得できます。
PasswordService ps = new DefaultPasswordService();
String encryptedPassword = ps.encryptPassword(yourPassword);
ハッシュ化されたパスワードは以下のような形式です。
将来的にSHA-256でも弱くなってきたとき、パスワードフォーマットにハッシュアルゴリズム名が入っているので、SHA-256が含まれるパスワードを持つユーザ全てにパスワード再設定依頼のメールを送るなどの使い方ができます。
$shiro1$SHA-256$500000$DaYxi7JkOR5asUxrMJVZiQ==$ELBJExhsFc+RiiV8uqGF5z/8DvoPZzzo8mXZHkWGVh0=
フォーマット
$shiro1$ハッシュアルゴリズム名$イテレーション回数$base64エンコードされたソルト$base64エンコードされたパスワード
このパスワード化されたハッシュをJPAなどを使ってDBMSに保存します。
DefaultPasswordServiceを使ってハッシュ化されたパスワードを生成した場合は、設定例で紹介したようにshiro.iniには認証時に使うハッシュとしてDefaultPasswordServiceを設定しなければいけません。ハッシュパスワード生成時と、認証処理の照合時で同じアルゴリズムでないと同一性が判断できないためです。
パスワードの中にソルト入ってて大丈夫?
安全性とプログラミングの煩雑さのトレードオフだと、徳丸先生からのアドバイスがあったので、扱う情報に応じてDefaultPasswordServiceではなくて、独自に実装したものを使うと良いと思います。
認証・認可
認証・認可はとても簡単にできます。
認証
UserNamePasswordTokenクラスにユーザIDと平文パスワードの組を設定し、Subjectに渡して判定します。
このloginメソッドの内部で、shiro.iniで設定したレルム設定に基づき、引数パスワードのハッシュ化と、DBMSからハッシュパスワードの取得および照合を行います。
// Shiroによる認証コード (再掲) Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(email, password); try { currentUser.login(token); } catch (AuthenticationException ae) { // authentication failed. }
認証に成功すると、これ以降にsubject.isAuthenticated()
を実行するとtrueが返るようになります。未認証の場合はfalseです。
// 認証済みかの判定 Subject currentUser = SecurityUtils.getSubject(); if (currentUser.isAuthenticated()) { // already login } else { // not logged in }
認証に失敗した場合は、AuthenticationExceptionの子クラス例外がエラー要因に応じて投げれられます。
あまり認証エラー理由を細かくユーザにフィードバックするのも良くないので、AuthenticationExceptionをキャッチしても良いと思います。
認可
認可もかんたんです。hasRoleメソッドで、現在アクセスしているユーザが引数のロールを持っているか判定します。
Subject currentUser = SecurityUtils.getSubject(); if (currentUser.hasRole("yourRoleName")) { // hasRole } else { // not hasRole }
このhasRoleメソッドの内部でも同様に、shiro.iniで設定したレルム設定に基づいてロール情報をDBMSから取得して照合しています。
ログアウト
ログアウトもシンプルで、logoutメソッドを実行するだけです。
Subject currentUser = SecurityUtils.getSubject(); currentUser.logout();
セッション操作
ServletのセッションAPIに似ていますが、セッションの終了だけはsession.invalidate()ではなく、session.stop()
です。
Subject current = SecurityUtils.getSubject(); // セッションの取得。既存セッションが無ければ新規生成。 Session session = current.getSession(); // 既存セッションがなければnullを返す。 Session session1 = current.getSession(false); // 属性の取得・設定 int cnt = (Integer)session.getAttribute("counter"); session.setAttribute("counter", 1); // セッションの終了 session.stop();
Shiroはスタンドアロン環境でも使えるようにShiro固有のセッション管理機構を持っていますが、Servletコンテナで動作させた場合はデフォルトでServletコンテナのセッションが使われます。
このため、セッションを取得するといつも通り、クッキー名JSESSIONIDがSet-Cookieヘッダに載ってきます。
Webアプリで使う
フィルタの設定
ApacheShiroはスタンドアロンのアプリでも使えるらしいですが(試してない)、Webアプリで使う場合は、web.xmlに以下の設定が必要です。
<filter> <filter-name>ShiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>ShiroFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping> <listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener>
ShiroFilter
の設定が漏れていると、SecurityUtils.getSubject()
を実行したときに現在のコンテキストを取得できないと以下のような例外が返ってきます。dispatcherの部分はforwardなどのリクエストを伴わない遷移時にもフィルタが有効になることを意図しています。
org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration. at org.apache.shiro.SecurityUtils.getSecurityManager(SecurityUtils.java:123) at org.apache.shiro.subject.Subject$Builder.<init>(Subject.java:627) at org.apache.shiro.SecurityUtils.getSubject(SecurityUtils.java:56)
EnvironmentLoaderListener
は、アプリ起動時にshiro.iniをロードしています。
カスタムJSPタグ
認証OK/NGでログインフォームへのリンクを表示するか、Webcome ユーザ名を表示するか分けたい、ロールに応じて出力情報を変えたいなど、認証・認可とテンプレートは密接な関係があります。
ShiroではJSPを対象にカスタムタグを用意しています。GitHub上では、Thymeleaf向けの拡張を作っている人もいるみたいです。
認証タグ
<shiro:authenticated>
は、認証済みの場合のみ表示されます。
<shiro:principal />
で認証済のユーザ名を取得することもできます。
<shiro:notAuthenticated>
は未認証の場合のみ表示です。
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <shiro:authenticated> <h3>Hello, <shiro:principal/></h3> </shiro:authenticated> <shiro:notAuthenticated> <h3>Hello, Guest</h3> </shiro:notAuthenticated>
認可タグ
<hasRole>
タグでは、ロール名を指定して表示可否を設定できます。
<shiro:hasRole name="MANAGER"> <!-- 大事な情報 --> </shiro:hasRole>
Remember me
ログインフォームには、以下のようにブラウザを閉じた後でも状態を保持するようにkeep me signed inのようなチェックボックスがあると思います。
ShiroではRemember me機能として、この機能の仕組みを提供しています。
このRemember meを有効にするためには、ログイン時のUsernamePasswordTokenのコンストラクタの引数にtrue/falseを追加するだけです。trueを設定するとRemember meが有効になります。
Subject currentUser = SecurityUtils.getSubject(); // remember me の有効化 UsernamePasswordToken token = new UsernamePasswordToken(email, password, true); try { currentUser.login(token); ...
remember meを有効にした状態でログインすると、ユーザにはクッキー名rememberMeが返されます。ブラウザを閉じても、再度アクセスしたときにこのrememberMeクッキーが送られることで、状態を保持しています。
ログアウトすると、サーバよりMax-Age=0
が設定されたrememberMeが返され、クッキーは削除されます。
注意が必要なのは、認証済み状態と、Remember Me状態は異なることです。例えば、JSPタグ<shiro:authenticated>
で囲った部分はRemember Me状態では表示されません。
Remember Me状態は、Amazonでいうと買い物かごの操作はできるが、購入処理ができない状態を示しており、購入処理をするためには再度認証処理が必要です。
Remember Me状態は、APIではSubject.isRemembered()
で判定できます。
Subject currentUser = SecurityUtils.getSubject(); if (currentUser.isRemembered()) { ...
JSPカスタムでは、<shiro:user>
タグで囲ってある範囲が、Remember me状態で表示されます。
<shiro:user> <h3>Welcome back <shiro:principal/> !</h3> <p>Not <shiro:principal/> ?</p> <p>Please signin <a href="#">here</a></p> </shiro:user>
その他
サンプルコードでは使っていないため詳細は紹介しませんが、form認証のように設定ファイルに認証対象のパスを書いて宣言的に設定する機能や、ロールよりももっと詳細に認可管理できるPermission機能など、必要そうな機能は一通り揃っています。
サンプルコード
以下のような画面を持つサンプルを作って公開してみました。上記で紹介した色々な機能がこのコードに盛り込まれています。ぜひShiroを使ってみようと思っている方は参考にしてみてください。
- 左上のSign Up
- ShiroのPasswordService機能を使って、入力されたパスワードをハッシュ化して、DBに保存しています
- 右上のSign In
- ShiroのAPIを使ってログインしています。ブラウザを閉じた後もセッションを保持するRemember meも実装しています。
- 下のAutholizationのセクション
- managerロールを持つアカウントでログインすると、登録されているユーザ一覧が表示されます。
まとめ
- Apache Shiroは比較的シンプルに使えるセキュリティ管理ライブラリです
- とはいえ、shiro.iniはそれなりに設定が必要です
- APサーバの固有設定に依存せずにレルム設定できるのは嬉しいところ
- ここまで書いておいて、そもそも認証・認可を各アプリで作り込むべきか?
- まだ追いつけてませんが、OpenID Connectのような仕組みを使って、認証をアプリ外に委譲するのが、社内システムでも一般的になっていくのでしょうか
- え? Java EEじゃない? サンプルコードの環境にGlassFish4を使ってるのでお許しを
MavenのJAX-WSプラグイン
まだまだエンタープライズな分野ではJAX-WSがシステム間連携に使われることが多い。wsdlからスタブを生成するMavenプラグインの使い方に関するメモ。
JAX-WS Mavenプラグイン
https://jax-ws-commons.java.net/jaxws-maven-plugin/ にJAX-WSの参照実装Metroの一部として提供されている。最新バージョンは2.3。
pom.xml の書き方
wsimport (wsdlからスタブコード生成) の例。設定可能なパラメータの一覧より、よく使う部分だけ以下に記載する。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.agetsuma</groupId> <artifactId>SOAPClient</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <build> <plugins> <plugin> <groupId>org.jvnet.jax-ws-commons</groupId> <artifactId>jaxws-maven-plugin</artifactId> <version>2.3</version> <executions> <execution> <goals> <goal>wsimport</goal> </goals> </execution> </executions> <configuration> <!-- .wsdlの格納ディレクトリ。デフォルト ${basedir}/src/wsdl --> <!-- <wsdlDirectory>src/main/wsdl</wsdlDirectory> --> <!-- スタブ生成対象のwsdlファイル名。wsdlDirectory配下のファイル名を指定。 定義がなかった場合はwsdlDirectory配下すべての.wsdlが対象 --> <!-- <wsdlFiles> <wsdlFile>HelloWorld.wsdl</wsdlFile> </wsdlFiles> --> <!-- 中間生成ファイルを保持するか。具体的にはスタブのソースコードのこと。 デフォルトはfalseで.classファイルのみ生成する。 --> <keep>true</keep> <vmArgs> <vmArg>-Djavax.xml.accessExternalSchema=all</vmArg> </vmArgs> </configuration> </plugin> </plugins> </build> </project>
このプラグインはデフォルトでフェーズgenerate-source
(コンパイルする前) *1 に動くため、中間生成ファイルとして出力されるソースコードは破棄しても問題ないが、デバッグ目的で生成しておくと便利。
JDK8ではvmArgsの設定がないと例外が出て動かない。詳細についてはNetBeansのBug 241570や、このMavenプラグインのJIRA JAX_WS_COMMONS-129で紹介されている。
ビルド
いつも通りmvn package
でビルドできる。
mvm clean package
PDFBoxで取得したテキストの空白置換
SlideShareがやっているように、PDFスライドから抽出したしたテキストから、改行やタブを半角スペース1つに置換を試みた際にはまったのでメモ。
取得したいのはSlideShareのページ下部に表示されているこの文字列。
1. PDFBoxでテキスト抽出
PDFTextStripper.getText(PDDocument doc) がIOExceptionを投げるため、IntStreamでループを回すとforEachブロック内での例外処理が強制される。シンプルにするためにfor文で書いてみる。
PDDocument doc = PDDocument.load("/Path/PDFSlide.pdf"); List<PDPage> pages = doc.getDocumentCatalog().getAllPages(); PDFTextStripper stripper = new PDFTextStripper(); for (int i = 1; i <= doc.getNumberOfPages(); i++) { stripper.setStartPage(i); stripper.setEndPage(i); System.out.println(stripper.getText(doc)); }
この時点では以下のようにタブや空白、改行が混じったテキストが抽出される。
【JJUG CCC 2014 Spring H-‐2】 Javaトラブルに備えよう 日本Javaユーザグループ 上妻 宜人 (あげつま のりと) はてなブログ : n-agetsuma.hatenablog.com
2. 複数の空白やタブ・改行を半角スペースに置換
Javaで空白を置換する場合、replaceAllに¥sを設定する例が非常に多く紹介されているが、これではうまくいかないことがある。
// タブや改行を空白に置換し、2つ以上スペースが連続する場合は1つに変換 // ¥t タブ, ¥r 復帰, ¥n 改行, ¥r¥n 改行, ¥f 改ページ, 全角スペース String text = stripper.getText(doc).trim() .replaceAll("\\t|\\r|\\n|\\r\\n|\f| ", " "); .replaceAll("\\s{2,}", " ");
JJUG と CCC の間の連続したスペースが消えない。
【JJUG CCC 2014 Spring H-‐2】 Javaトラブルに備えよう...
調べてみると、JJUG{¥u0020}{¥u00a0}CCC
となっており、¥u0020はおなじみの半角スペースだが、¥u00a0(No-Break Space)が途中に挟まっている。
¥u00a0(No-Break Space)は、wikipediaに解説があり、HTMLでいう& nbspで、行末にスペースがある場合においてスペースと前の語の間で改行させないために使う文字。スライドの文字列には改行のコントロールが含まれているため、テキスト抽出すると普通のスペースではなく、nbspに変換される場合があるようだ。
他のスライドには同様な文字 ¥u202F (Narrow No-Break Space) も含まれていた。
対処
¥u00a0 (No-Break Space) と ¥u202F(Narrow No-Break Space) もスペースへの置換対象に加えてみた。
String text = stripper.getText(doc).trim() .replaceAll("\\t|\\r|\\n|\\r\\n|\f| |\u00a0|\u202F", " ") .replaceAll("\\s{2,}", " ");
1つの空白区切りで抽出できるようになった。
// before 【JJUG CCC 2014 Spring H-‐2】 Javaトラブルに備えよう... // after 【JJUG CCC 2014 Spring H-‐2】 Javaトラブルに備えよう ...
JavaOne2014 4日目メモ (10/1)
サンフランシスコ市街地のスーパーに水を買いにいったら、キャベツ太郎が売ってたので思わず写真を撮る。2.49ドル、日本でいうとポッキーよりも高級なお菓子になっている。
以下のようなセッションに参加。
Everything You Wanted to Know About Writing Async, Concurrent HTTP Apps in Java [CON3712]
非同期でかつノンブロッキングI/Oを行うHTTPクライアントをどうやって書くか紹介するセッション。
非同期およびノンブロッキングに関する振り返り
- 非同期
- I/O中のスレッドはブロックされるが、完了前に応答を返してくれるので別の仕事が並行でできる
- javaではFutureが帰ってくるような実装で実現
- ノンブロッキング
- I/O中にスレッドをブロックしない。selectorに登録し、データ利用可能時に通知をもらうモデル
- javaではNIOパッケージで実装できる
今のところの実現手段
- 元々はSNSサイトのNing社が作ってたライブラリ async-http-client
- Apache HttpCompoinentにある機能 HttpAsyncClient
いろんなREST APIから情報を持ってきて組み立てるシステムでは、毎回同期でかつブロックして待ってたら遅いので、非同期かつノンブロッキングが比較的簡単に実装できるのはとても大切。
Principles of Evolutionary Architecture [CON4580]
継続的にアーキテクチャを進化させていく上での原理・原則を紹介するセッション。
進化するアーキテクチャの原則
- 最終責任時点 (last responsible moment)
- その時点で集まる最大限の情報を集め、複雑性による技術的負債は最小限に
- 優先度を重視して、早めに決断する
- 発展的なアーキテクトと開発
- データのライフサイクルとオーナーを明確にする
- 軽量なツールとドキュメント
- ソフトウェアの内部品質は変更が容易かどうかで評価すべき
- ポステルの法則
- 送信側は慎重に、受信側は寛容に。バリデーションは必要な範囲で。
- 設計に対するテスタビリティ
- テスタビリティを向上させることで、より最適な設計に近づく
- メッセージングシステムは、あくまで通信のために使い、ビジネスロジックとしては使わない
- 振る舞いや性能だけでなく、規約もテストする
- コンウェイの法則
- それぞれの組織がデザインするシステムは、その組織間のコミュニケーション構造を反映する
- コミュニケーションの欠落は、複雑な統合に繋がる
- 製品は組織と同じ形になるので、変えたければ組織か製品自体を変える
実現のためのテクニック
- データベースリファクタリング
- 大きな変更を小さく分割して、バージョン管理もする
- 継続的デリバリー
- デプロイって作業は本来退屈じゃなきゃいけない!
- システムインタフェース規約のテスト
- 個々のシステムが独立して作業できるようなインタフェース規約
- ポステルの法則も意識しながら
- 古来よりアーキテクトが持つ役割の一つ
今後の仕事で判断に迷ったときの参考にしようと思う。
Plugging into the Java Compiler [CON4265]
Googleが作ってるValueObject実装のAutoValue、Squireが作ってるDIの実装daggerと、それぞれの技術の基となっているアノテーションプロセッサの話
AutoValue
- イミュータブルなValueObjectのコード記述を簡潔にすることが目的
pom.xmlには以下を追加
<dependency> <groupId>com.google.auto.value</groupId> <artifactId>auto-value</artifactId> <version>1.0-rc1</version> <scope>provided</scope> </dependency>
@AutoValueを付与する
@AuthValue public abstract class Address { public abstract String streetAddress(); public abstract int postCode(); public static Address create (String atreetAddress, int postCode) { return new AutoValue_Address(streetAddress, postCode); } }
ValueObject生成時にはcreateメソッドを実行する
Address a = Address.create("some address", "xxx-xxxx");
lombokの@Value異なり、AutoValueではコンパイラの処理に割り込んでバイトコードを追加したりなんてことはせずに、シンプルにソースコードを生成する。実際にNetBeansで試してみると、以下のようにソースファイルが出てくる。
AutoValueを使うとあくまでソースを生成しているだけなので、仮にAutoValue自体にバグがあってもすぐわかる。lombokで得る効率よりも、バイトコード生成の黒魔術感への不安が勝るプロジェクトで使えそう。
dagger
dagger1.0のコード例についてはマニュアルが詳しい。
Debt and Deprecation [CON6377]
Java Day Tokyo 2014やJJUG CCC 2014 Springでもラムダ式やストリームAPIについて講演されたStuart Marksさんが、Dr.Deprecator(非推奨博士?)として登場。
内容はJavaの『@Deprecated - 非推奨API』に関する振り返りと、現在複数の意味合いを持つDeprecatedを新しい言葉に置き換える提案を紹介するセッション。
今までの非推奨の振り返り
既存の『非推奨』の使い方
非推奨を新しい言葉を使って再カテゴライズする提案
陳腐化(Obsolescence)したAPIは現在の非推奨API以外(例 Vector)にもたくさんある。非推奨と一つに丸めてしまうのではなく、適切な用語を再定義してカテゴライズする。
- Condemned : 将来のリリースで削除や無効化するもの
- Dangerous : バグやデータ損失を招くAPIであるもの
- Superseded : 危険ではなく、削除される予定もないが、新しいコードでは新機能に置き換えた方が良いもの
- Unimplemented : 実装されてないことを示す。UnsupportedOperationExceptionをランタイム例外としていきなり返さずに、ドキュメント上でわかりやすくする
例えば、Superseded(廃止)として現状は非推奨になっていないが、VectorやHashTableなどの古いコレクションクラスや、java.util.Timer/TimerTask(タスクが長時間かすると次の実行がずれるシングルスレッドモデルなので、ScheduledThreadPoolExecutorに置き換えた方が良い)がある。