認証機能が存在するWEBアプリケーションを開発する場合、認証に関連する機能をゼロから開発するのは大変ですね。
ちょっと考えただけで、以下のような制御が必要になります。
- ログイン認証
- セッション管理
- 不正アクセスに対する制御
- ワンタイムトークン
これらの、いわゆる認証で必要な機能を提供しているのが「SpringSecurity」です。
簡単な実装と設定ファイルへの設定記載をおこなえば、認証に必要な最低限の機能が使えるようになります。
今回は、認証機能が存在するWEBアプリケーションに対するSpringSecurityの使いかたを説明していきます。
ログイン認証
ログイン認証に関して、SpringSecurityでの実装はお決まりです。
applicationSecurity.xmlの基本的な定義
まずは、「applicationSecurity.xml」にSpringSecurityを使用するための定義が必要です。
以下に、定義例を記述します。
<sec:form-login login-page="/xxx.jsp" username-parameter="username" password-parameter="password" default-target-url="/xxx" always-use-default-target="true" login-processing-url="/j_spring_security_check" authentication-failure-handler-ref="authenticationFailureHandler" authentication-success-handler-ref="authenticationSuccessHandler" /> <bean id="authenticationSuccessHandler" class="jp.co.xxx.AuthenticationSuccessHandler"> <property name="alwaysUseDefaultTargetUrl" value="true" /> <property name="defaultTargetUrl" value="/xxx/xxx" /> </bean> <bean id="authenticationFailureHandler" class="jp.co.xxx.ExceptionMappingAuthenticationFailureHandler"> <property name="defaultFailureUrl" value="/xxx" /> </bean>
「xxx」の箇所は、SpringSecurityを組み込むWEBアプリケーション固有の部分になります。
<sec>タグで、SpringSecurityの基本動作を定義しています。
<sec>タグで、認証成功時と認証失敗時の制御クラスを定義しています。
「authenticationSuccessHandler」「authenticationFailureHandler」です。
その2つのクラスについて、<bean>タグでクラスの属性等を定義しています。
認証でのJava側の実装
Java側の実装においてポイントとなるのは、UserDetailsServiceクラスです。
UserDetailsServiceクラスのJavaDocは以下です。
で、UserDetailServiceを使った認証ロジックは以下になります。
import javax.servlet.http.HttpServletRequest; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String loginID) { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); HttpServletRequest request = (HttpServletRequest) attrs.resolveReference(RequestAttributes.REFERENCE_REQUEST); String password = request.getParameter("password"); } }
「loadUserByUsername」のパラメータである「loginID」が、ログイン画面で入力されたログインID。
HTTPリクエストから取得している「password」が、ログイン画面で入力されたパスワードです。
この実装をおこなうこで、「loadUserByUsername」でログインIDとパスワードがわかります。
後は、ログインIDとパスワードが格納されているテーブルを検索して、ログインIDとパスワードの一致判定をやればOKです。
ログインIDとパスワードが一致しているユーザ情報の取得でさえも、SprinSecurityが機能を提供をしています。
以下のような実装をおこなえば、DBに登録されているログインID・パスワードと一致するユーザ情報を取得できます。
import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; public class MyUserDetails extends User { public MyUserDetails( String userName, String password, String dispName, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<GrantedAuthority> authorities) { super(userName, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); } }
※SpringSecurityは、ユーザロール(ユーザ毎の権限制御)についても制御可能ですが、今回のサンプルでは省略します。
上記クラスを作成し、「loadUserByUsername」において「MyUserDetails」のインスタンスを取得します。
以下のような感じです。
user = new MyUserDetails( ※ログインID, ※パスワード, ※ユーザ名, true, true, true, true, authorities);
インスタンスが取得できたら認証成功、インスタンスが取得できなかったら認証失敗、です
システムによっては認証が成功したとしても、ログイン成功とはみなさないケースもあるかと思います。
例えば、アカウントロック。
認証失敗した数をDBに保存しておいて、その数が規定値を超えたら画面にエラーメッセージを表示してログインさせない。
そういったシステム固有の認証チェックについても、この「loadUserByUsername」に実装するとよいでしょう。
認証に成功したら、ログイン情報をセッションに格納します。
UserSession userSession = new UserSession(); userSession.setLoginID(※ログインID); userSession.setUserName(※ユーザ名); RequestContextHolder.getRequestAttributes().setAttribute("session", userSession, RequestAttributes.SCOPE_SESSION);
userSessionというのは、システム固有でセッションとして保持しておきたい情報を格納するインスタンス。
そのインスタンスをセッションとして格納しています。
上記のサンプルプログラムは、ログインIDとユーザ名をセッションに格納する例です。
ここら辺はシステムによって変わってくるかと思います。
セッションとして保持するべきユーザ情報を見極めて、セッションに格納するようにしましょう。
認証に成功してセッションに情報を格納したら、認証については完了です。
ここまで記述した認証をおこなうためには、前述したSpringSecurityの定義の他に、「applicationSecurity.xml」に以下の定義が必要です。
<sec:authentication-manager> <sec:authentication-provider user-service-ref="myUserDetailsService" > <sec:password-encoder ref="passwordEncoder" /> </sec:authentication-provider> </sec:authentication-manager> <sec:global-method-security pre-post-annotations="enabled"/> <bean id="myUserDetailsService" class="jp.co.xxx.MyUserDetailsService" /> <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />
「jp.co.xxx.MyUserDetailsService」の「xxx」部分は、このクラスがシステム固有のクラスになるために「xxx」にしています。
システム固有のパッケージになります。
「jp.co.xxx.AuthenticationSuccessHandler」「xxx」部分は、このクラスがシステム固有のクラスになるために「xxx」にしています。
システム固有のパッケージになります。
この「AuthenticationSuccessHandler」に、ログイン成功時の制御を記述します。
固有のログを出すとか、ログイン成功回数をカウントするとか、業務に合わせた制御について、このクラスに処理を記述すればよいかと思います。
「value=”/xxx/xxx”」が、ログイン成功時に遷移するURLです。
デフォルトのURLになりますので、例外的に他URLに遷移させたい場合は、「AuthenticationSuccessHandler」で他URLに遷移する制御をおこなえばよいです。
パスワードの暗号化
パスワードはDBに登録することになりますが、一般的に平文(そのままの文字列)で登録することはセキュリティの観点からNGです。
何かしら暗号化して登録することが一般的です。
SpringSecurityは、暗号化されたパスワード文字列を参照しての認証についても自動で行ってくれます。
暗号化の種類は選べるのですが、以下の例は「SHA-1」で暗号化(ハッシュ化)された文字列をパスワードとして扱う場合の例です。
以下の定義を「applicationSecurity.xml」に定義すればよいです。
<bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder" > <constructor-arg value="1"/> </bean>
「constructor-arg」がSHA強度です。
「1, 256, 384, 512」といった値を設定するのが一般的。
「applicationSecurity.xml」に定義するのは認証時の制御について必要になるのですが、パスワード文字列を登録する処理は別途開発が必要です。
多分、ほとんどのシステムがユーザ登録画面があり、そこからパスワード文字列を登録することになるかと思うので、その制御にパスワード文字列を暗号化してDBに登録する制御が必要になります。
SpringSecurityとは別に開発が必要ですが、難しい処理ではないです。
public static String getHashString(String str) { // ダイジェスト文字列取得 MessageDigest md = null; try { md = MessageDigest.getInstance("SHA1"); } catch (NoSuchAlgorithmException e) { return ""; } // ダイジェスト文字列をハッシュ文字列に変換する byte[] dat = str.getBytes(); md.update(dat); byte[] byt = md.digest(); StringBuffer buff = new StringBuffer(byt.length * 2); for (int lc = 0; lc < byt.length; lc++) { int d = byt[lc] & 0xFF; if (d < 16) { buff.append("0"); } buff.append(Integer.toHexString(d)); } return buff.toString(); }
実際のシステムでは、ユーザ情報の登録編集時に上記のようなロジックを組み込み、DBに暗号化されたパスワード文字列を登録します。
不正アクセスに対する制御(フィルター)
SpringSecurityを使って認証をおこなった場合、ログイン後の画面は認証に成功した状態じゃないとアクセスできません。
しかし、SpringSecurityの対象外にしたい画面があったります。
例えば以下のような画面。
- ログインしてなくても使える画面(パスワード再発行画面、など)
- CSSやJSファイル
上記のようにSpringSecutiryの対象外にしたい画面やファイルについては、「applicationSecurity.xml」に定義をおこなうことで設定できます。
フィルター定義ですね。
<!-- SpringSecurity Filter対象外URL設定 --> <sec:http pattern="/css/**" security="none"/> <sec:http pattern="/js/**" security="none"/>
上記定義例は、ルートフォルダにある「css」フォルダと「js」フォルダ直下の全てのファイルについて、SpringSecurityの対象外にしています。
この定義をおこなわないと、画面にCSSが反映されず、かつ、JavaScriptも動かないので必須の定義になります。
CSRFトークン
SpringSecurityは、CSRFチェックをおこなうトークン値を自動で発行してくれます。
CSRFはクロスサイトリクエストフォージェリ”のことで、サイバー攻撃の一種です。
CSRFの詳しい説明は、以下のサイトにきれいまとまっています。
要はリクエストの改ざんです。
悪意のある第三者が、リクエストを偽造してWEBサイトにアクセスするサイバー攻撃になります。
これを防ぐ手段の一つがCSRFトークンです。
HTTPリクエスト内でCSRFトークンというシステムが発行した文字列を受け渡します。
このトークン値がシステムが期待している値と違ったら不正なアクセスと判断し、SpringSecurityがアクセスを遮断します。
ちょっと小難しいですが、こういった制御をSpringSecurityは自動でやってくれます。
JSP内にCSRFトークンを埋め込む処理を実装するだけです。
具体的には、以下の記述をおこないます。
これはJSPの場合、ですが。
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
基本はログイン画面でのみ埋め込みばよい。
SpringSecurityは、どうやら1回発行したトークン値を使い回すようです。
業務システムだとブラウザの戻るボタンを許可しない場合もありますが、その場合は上記のコードを全画面のJSPの埋め込む必要があります。
全ての画面に埋めこむと、戻るボタンで戻った先の画面のCSRFトークン値が古くなってしまい、トークン値不一致でSpringSecurityが不正アクセスと判断します。
仮に不正アクセスが発生した場合の制御については、「applicationSecurity.xml」に定義したクラスで制御可能。
<bean id="requestDataValueProcessor" class="jp.co.xxx.CompositeRequestDataValueProcessor"> <property name="processors"> <list> <bean class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" /> </list> </property> </bean>
まとめ
いかがでしたでしょうか?
世の中に存在するほぼすべてのWEBシステムが認証機能をもっているかと思いますが、認証機能をゼロから作るのは骨が折れます。。。
SpringSecurityを導入すれば、認証に必要な機能をゼロから作る必要はありません。
XMLの定義で結構必要なところが解り辛い部分なのですが、使わない手はないかと。
それではまた!