====== Spring rememeber-meログイン ====== ログインを維持してくれるremember-meログイン機能をSpring securityを用いて実装する方法を紹介します。\\ 実装形態は次節で説明しますが、ここでは、persistence token実装形態を選択しています。 {{keywords>Spring rememeber-me login}} ====== remember-me実装形態 ====== remember-me実装形態について、以下の2つがあります。\\ 1)cookie-base実装\\ remember-meクッキーにusernameとpassword(MD5hashコード)保持 ((クッキーがcaptureされる場合、username, passwordが変わらない限り、有効期限内であれば使用可能な脆弱性があります。)) 2)persistence token実装\\ persistent_loginsテーブルにランダムなserial, tokenを保持、clientのremember-meクッキーと照合します。\\ remember-meクッキーには一意なserialを保持していてアクセスの度にtokenを変える仕組みなのでクッキーがcaptureされてもアクセスされる可能性は低いです。\\ パスワードが変更になってもtokenが有効であれば使用可能です。\\ logoutしない限り、有効期限切れになったtokenはpersistent_loginsテーブルにレコードが残ります。\\ 有効期限切れになったtokenを削除する機能はSpring securityに実装されてないので、自前実装が必要です。 ====== persistent_loginsテーブル ====== テーブル定義SQLを下記に示します。 CREATE TABLE "PERSISTENT_LOGINS" ( "USERNAME" VARCHAR2(100 BYTE) NOT NULL, "SERIES" VARCHAR2(64 BYTE), "TOKEN" VARCHAR2(64 BYTE) NOT NULL, "LAST_USED" TIMESTAMP (6) NOT NULL, CONSTRAINT "PERSISTENT_LOGINS_PK" PRIMARY KEY("SERIES") ) 以下はテーブルについてのメモです。\\ ・テーブル名、カラム名は変更できない。\\ ・同一usernameでも別端末からアクセスすると別レコード(series)が生成される。\\ ・ログアウトするとレコードは削除される。\\ ・同一端末でも開発環境、検証環境にrememeber-meログインする場合、2レコードが作成される。\\ ====== persistent remember-me実装 必要ライブラリ ====== 以下にpersistent rembember-me実装で必要なライブラリを示します。\\ ^ライブラリ^説明^ |jcl-over-slf4j-1.7.36.jar|spring security logging関連| |log4j-slf4j-impl-2.17.1.jar|spring security loggingをlog4j2.xmlにて設定可能にする| |slf4j-api-1.7.32.jar|spring security logging関連| |spring-web-4.3.30.RELEASE.jar|spring-web| |spring-core-4.3.30.RELEASE.jar|spring-webの依存ライブラリ| |spring-aop-4.3.30.RELEASE.jar|spring-webの依存ライブラリ| |spring-beans-4.3.30.RELEASE.jar|spring-webの依存ライブラリ| |spring-context-4.3.30.RELEASE.jar|spring-webの依存ライブラリ| |spring-expression-4.3.30.RELEASE.jar|spring-contextの依存ライブラリ| |spring-jdbc-4.3.30.RELEASE.jar|spring-jdbcライブラリ| |spring-ldap-core-2.3.3.RELEASE.jar|spring-security-ldap依存ライブラリ| |spring-security-config-4.2.20.RELEASE.jar|spring-security configライブラリ| |spring-security-core-4.2.20.RELEASE.jar|spring-security-ldap依存ライブラリ| |spring-security-ldap-4.2.20.RELEASE.jar|spring-secruity ldapライブラリ| |spring-security-web-4.2.20.RELEASE.jar|spring-security webライブラリ| |spring-tx-4.3.30.RELEASE.jar|spring-jdbc依存ライブラリ| ====== 実装編 ====== 以下のセクションは実装について説明します。 ===== web.xml ===== web.xmlに追加する設定についてです。\\ 下記、追加コード部分を抜粋して表示します。 org.springframework.web.context.ContextLoaderListener contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext contextConfigLocation ★1 com.contoso.web.spring springSecurityFilterChain springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy ★2 springSecurityFilterChain action ★1\\ Spring Security関連Annotationが設定されているクラスのパッケージを指定します。\\ ★2\\ Filter Chainの中でSpring Securityをinterceptする部分です。 ===== PersistenceConfig.java ===== persitent_loginsテーブルへのデータソース接続情報を読み込ませるためのBeanを定義しているクラスになります。 package com.contoso.web.spring; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.transaction.annotation.EnableTransactionManagement; /** *

* Spring Database Configuration. *

* @author ri-su */ @Configuration @EnableTransactionManagement @PropertySource({"classpath:/com/contoso/base/properties/db/persistence-oracle.properties"}) ★1 public class PersistenceConfig { @Autowired private Environment env; ★2 //***** public method ***** @Bean public DataSource dataSource() { final DriverManagerDataSource _dataSource = new DriverManagerDataSource(); ★3 _dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName")); _dataSource.setUrl(env.getProperty("jdbc.url")); _dataSource.setUsername(env.getProperty("jdbc.user")); _dataSource.setPassword(env.getProperty("jdbc.pass")); return _dataSource; } //***** protected method ***** //***** private method ***** //***** call back method ***** //***** getter and setter ***** }
★1\\ persistence-oracle.propertiesのパスを指定します。\\ ★2\\ Springの@Autowired機能で@PropertySourceで指定したPropertiesファイルにアクセスできます。\\ ★3\\ アプリ起動時にBeanインスタンス化され、後ほど説明するwebSecurityConfig.xmlからデータソースを参照できます。\\ 参照名はメソッド名であるdataSourceです。 ===== SecurityConfig.java ===== Spring security設定xmlファイル(webSecurityConfig.xml)を指定するクラスです。\\ WebSecurityConfigurerAdapterクラスを拡張して作成します。 package com.contoso.web.spring; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** *

* Spring Security Configuration. *

* @author ri-su */ @Configuration @ComponentScan("com.contoso.web.security") ★1 @EnableWebSecurity @ImportResource({"classpath:/WEB-INF/webSecurityConfig.xml"}) ★2 public class SecurityConfig extends WebSecurityConfigurerAdapter { //***** constructor ***** public SecurityConfig() { super(); } //***** public method ***** //***** protected method ***** //***** private method ***** //***** call back method ***** //***** getter and setter ***** }
☆1\\ webSecurityConfig.xmlからComponentとして参照したいクラスが位置しているパッケージを指定します。\\ ☆2\\ webSecurityConfig.xmlのパスを指定します。 ===== webSecurityConfig.xml ===== Spring security設定ファイルです。 ★1 ★2  ★3 ★4 ★5 ★6 ★7 ldap://ldap001.contoso.com:3268 ldap://ldap002.contoso.com:3268 ★8 ★9 ★10 ★1\\ ログインUrl、ログイン成功Handlerクラス、認証失敗時の遷移Url等を設定します。\\ 認証成功HandlerクラスであるmySavedRequestAwareAuthenticationSuccessHandlerについては後ほど説明します。\\ ★2\\ ログアウトUrl、ログアウト成功Handlerクラス等を設定します。\\ ログアウト成功HadlerクラスはSpringのSimpleUrlLogoutSuccessHandlerを利用しています。\\ ★3\\ remember-me関連設定を行います。token有効期限は秒(sec)で指定します。\\ SuccessHandlerについては後ほど説明します。\\ ★4\\ remember-me実装形態としてpersistent token実装を設定しています。\\ constructorのパラメータ1に指定しているmyAppKeyはクッキーに含まれる署名に使用されるキーです。\\ 省略する場合、SecureRandom関数でランダムに生成されますが、アプリケーション起動時に生成されるので再起動するとremember-meクッキーは全て無効になってしまいます。\\ アプリケーションを再起動してもクライアントが持つクッキーを有効にする場合は、任意の固定文字列を指定します。\\ ★5\\ tokenを保存するデータソース関連設定を行います。\\ dataSourceが参照しているBeanクラスはPersistenceConfig.javaです。 ★6\\ 認証マネージャーを設定しています。 ★7\\ ログイン時、LDAPを通して認証を行うことを設定しています。\\ LDAPドメインは複数指定が可能です。\\ ★8\\ 認証ProviderとしてLdapAuthenticationProviderを指定しています。\\ contextSourceには☆7を参照しています。\\ ★9\\ LDAP検索方法を指定しています。Person、SAMアカウント名で検索すると指定しています。\\ ★10\\ ★4で設定しているLdapUserDetailsServiceについて定義しています。 ===== MySavedRequestAwareAuthenticationSuccessHandler.java ===== webSecurityConfig.xmlにて参照名mySavedRequestAwareAuthenticationSuccessHandlerのクラスを以下に示します。 package com.contoso.web.security; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.ldap.userdetails.LdapUserDetails; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** *

* Custom implementation of SavedRequestAwareAuthenticationSuccessHandler *

* @author ri-su */ @Component(value = "mySavedRequestAwareAuthenticationSuccessHandler") public class MySavedRequestAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private static final Logger logger = LoggerFactory.getLogger(MySavedRequestAwareAuthenticationSuccessHandler.class); //ログイン画面直アクセスの場合の遷移先 private static final String DEFAULT_TARGET_URL = "/sample/Main.do"; //***** public method ***** /* (non-Javadoc) * @see org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.springframework.security.core.Authentication) */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { ★1 //authorization doAuthorization(request, authentication); //set defaultTargetUrl setDefaultTargetUrl(request); //delegate to super method super.onAuthenticationSuccess(request, response, authentication); } //***** protected method ***** //***** private method ***** //authorization private void doAuthorization(final HttpServletRequest request, final Authentication authentication) { //username String _username = ((LdapUserDetails)authentication.getPrincipal()).getUsername(); ★2 logger.debug("username: {}", _username); //do something here. 例)権限付与、セッションにユーザー情報保持 }//doAuthorization private void setDefaultTargetUrl(final HttpServletRequest request) { String _targetUrl = ""; //REQUEST_CACHEのredirectUrlをログイン画面のhiddenから取得 String _cachedUrl = request.getParameter(REDIRECT_URL_HIDDEN_NAME); String _referer = request.getParameter(REFERER_URL_HIDDEN_NAME); logger.debug("cached redirect url: {}", _cachedUrl); logger.debug("referer url: {}", _referer); //determine targetUrl if (StringUtils.hasText(_referer)) { _targetUrl = determineTargetUrlFromReferer(_referer); } else if (StringUtils.hasText(_cachedUrl)) { _targetUrl = _cachedUrl; ★3 } else { _targetUrl = DEFAULT_TARGET_URL; } logger.debug("target url: {}", _targetUrl); //set defaultTargetUrl super.setDefaultTargetUrl(_targetUrl); //always redirect to the value of defaultTargetUrl setAlwaysUseDefaultTargetUrl(true); }//setDefaultTargetUrl //refererUrl(ログアウト時)からtargetUrl判断 private String determineTargetUrlFromReferer(String referer) { String _targetUrl = ""; //some logic here ★4 return _targetUrl; }//determineTargetUrlFromReferer //***** call back method ***** //***** getter and setter ***** }
★1\\ onAuthenticationSuccessメソッドをオーバーライドします。\\ ここでは、認証成功後の権限付与(authorization)、遷移先などの設定を行います。\\ ★2\\ AuthenticationからLDAP認証のusernameが取得可能です。\\ usernameを用いてアプリ側のauthorization処理を実装します。\\ ★3\\ REQUEST_CACHEのredirectUrlをログイン画面のhiddenから取得します。\\ REQUEST_CACHEに残っていれば、そこに遷移します。\\ ★4\\ ログアウト時のurlがログイン画面のhiddenから取得できれば、ログアウト時の画面に遷移します。 ===== RememberMeAuthenticationSuccessHandler.java ===== webSecurityConfig.xmlにて参照名rememberMeAuthenticationSuccessHandlerのクラスを以下に示します。 package com.contoso.web.security; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.ldap.userdetails.LdapUserDetails; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; /** *

* authentication-success-handler in remember-me *

* @author ri-su */ @Component(value = "rememberMeAuthenticationSuccessHandler") public class RememberMeAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private static final Logger logger = LoggerFactory.getLogger(RememberMeAuthenticationSuccessHandler.class); private static final String DEFAULT_TARGET_URL = "/sample/Main.do"; private static final String LOGON_URL = "/logon.do"; private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); //***** public method ***** /* (non-Javadoc) * @see org.springframework.security.web.authentication.AuthenticationSuccessHandler#onAuthenticationSuccess * (javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.springframework.security.core.Authentication) */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { ★1 handle(request, response, authentication); clearAuthenticationAttributes(request); } //***** protected method ***** protected void handle(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException { //--- determine targetUrl final String _targetUrl = determineTargetUrl(request); if (response.isCommitted()) { logger.debug("Response has already been committed. Unable to redirect to " + _targetUrl); return; } //authorization doAuthorization(request, authentication); redirectStrategy.sendRedirect(request, response, _targetUrl); }//handle /** * determine target URL *
* @param request * @return */ protected String determineTargetUrl(final HttpServletRequest request) { String _targetUrl = request.getServletPath(); logger.debug("targetUrl: {}", _targetUrl); String _redirectUrl = ""; if (!Validator.isNullOrBlank(_targetUrl) && _targetUrl.indexOf(LOGON_URL) != -1) { ★2     _redirectUrl = DEFAULT_TARGET_URL; } else { _redirectUrl = _targetUrl; } return _redirectUrl; }//determineTargetUrl   /**   * Removes temporary authentication-related data which may have been stored in the session   * during the authentication process.   */ protected final void clearAuthenticationAttributes(final HttpServletRequest request) { final HttpSession _session = request.getSession(false); if (_session == null) { return; } _session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); }//clearAuthenticationAttributes //***** private method ***** //authorization private void doAuthorization(final HttpServletRequest request, Authentication authentication) { //username String _username = ((LdapUserDetails)authentication.getPrincipal()).getUsername(); ★3 logger.debug("username: {}", _username);         //do something here. 例)権限付与、セッションにユーザー情報保持 }//doAuthorization //***** call back method ***** //***** getter and setter ***** }
★1\\ onAuthenticationSuccessメソッドをオーバーライドします。\\ ここでは、認証成功後の権限付与(authorization)、遷移先などの設定を行います。\\ ★2\\ ログイン画面にアクセスする場合の遷移先を指定します。\\ ★3\\ AuthenticationからLDAP認証のusernameが取得可能です。\\ usernameを用いてアプリ側のauthorization処理を実装します。\\をを ===== NewLogon.jsp ===== Formログイン画面の例を以下に示します。 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ include file="/Taglibs.jsp" %>
Remember Me:
===== NewLogonAction.java ===== ログイン画面Actionの例を以下に示します。\\ ここではStruts1を利用したActionクラスの例です。 package com.contoso.web; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; /** *

* Action for Remember-me Login *

* @author ri-su */ public class NewLogonAction extends Action { //***** public method ***** @Override public ActionForward execute(ActionMapping mapping, AbstractActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception if (request.getParameter("error") != null) { ★1 ActionErrors _errors = new ActionErrors(); _errors.add("username", new ActionError("errors.logon.fail")); saveErrors(request, _errors); return mapping.getInputForward(); } return mapping.findForward(SUCCESS); } //***** protected method ***** //***** private method ***** //***** call back method ***** //***** getter and setter ***** }
★1\\ ログインエラー発生時、エラーメッセージをActionErrorにセットして画面に表示します。 ====== 参考情報 ====== 以下セクションで参考になる情報を載せます。 ===== Spring securityデフォルトレスポンスヘッダ ===== Spring remember-me実装にはSpring securityを利用していますので、Spring securityについて理解する必要があります。\\ Spring security XMLにて設定しなくてもデフォルトでサポートしているレスポンスヘッダをいかに示します。\\ ・Cache-Control(Pragma, Expires)((Spring SecurityはHTTP1.0互換のブラウザもサポートするために、PragmaヘッダとExpiresヘッダも出力する。))\\ ・X-Frame-Options\\ ・X-Content-Type-Options\\ ・X-XSS-Protection\\ ・Strict-Transport-Security((アプリケーションサーバに対してHTTPSを使ってアクセスがあった場合のみ出力される。))\\ デフォルト値について以下の表を参照してください。 ^Header name^value(default)^目的^ |Cache-Control|no-cache|コンテンツがキャッシュされるのを防ぐため| |X-Frame-Options|DENY|