目次
Spring rememeber-meログイン
ログインを維持してくれるremember-meログイン機能をSpring securityを用いて実装する方法を紹介します。
実装形態は次節で説明しますが、ここでは、persistence token実装形態を選択しています。
remember-me実装形態
remember-me実装形態について、以下の2つがあります。
1)cookie-base実装
remember-meクッキーにusernameとpassword(MD5hashコード)保持
1)
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に追加する設定についてです。
下記、追加コード部分を抜粋して表示します。
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <description> </description> <param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value> </context-param> <context-param> <description> </description> <param-name>contextConfigLocation</param-name> ★1 <param-value>com.contoso.web.spring</param-value> </context-param> <filter> <description> </description> <display-name>springSecurityFilterChain</display-name> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> ★2 </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <servlet-name>action</servlet-name> </filter-mapping>
★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; /** * <H3> * Spring Database Configuration. * </H3> * @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; /** * <H3> * Spring Security Configuration. * </H3> * @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設定ファイルです。
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sec="http://www.springframework.org/schema/security" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd"> <sec:http use-expressions="true"> <sec:intercept-url pattern="/js/**" access="permitAll" /> <sec:intercept-url pattern="/css/**" access="permitAll" /> <sec:intercept-url pattern="/images/**" access="permitAll" /> <sec:intercept-url pattern="/logon.do" access="permitAll" /> <sec:intercept-url pattern="/**" access="authenticated" /> <sec:form-login login-page="/logon.do" login-processing-url="/logon.do" authentication-success-handler-ref="mySavedRequestAwareAuthenticationSuccessHandler" authentication-failure-url="/logon.do?error=true" /> ★1 <sec:logout logout-url="/logout.do" success-handler-ref="simpleUrlLogoutSuccessHandler" delete-cookies="JSESSIONID" /> ★2 <!-- 86400 Seconds = 1day --> <sec:remember-me authentication-success-handler-ref="rememberMeAuthenticationSuccessHandler" data-source-ref="dataSource" token-validity-seconds="2592000" /> ★3 <sec:csrf disabled="true" /> </sec:http> <!-- Persistent Remember Me Service --> <bean id="rememberMeAuthenticationProvider" class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices"> ★4 <constructor-arg value="myAppKey" /> <constructor-arg ref="ldapUserDetailsService" /> <constructor-arg ref="jdbcTokenRepository" /> </bean> <!-- Uses a database table to maintain a set of persistent login data --> <bean id="jdbcTokenRepository" class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl"> ★5 <property name="createTableOnStartup" value="false" /> <property name="dataSource" ref="dataSource" /> </bean> <!-- Authentication Manager(LDAP) --> <sec:authentication-manager alias="authenticationManager"> ★6 <sec:authentication-provider ref="ldapAuthProvider" /> </sec:authentication-manager> <!-- LDAP context source --> <bean id="contextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource"> ★7 <constructor-arg index="0"> <list> <value>ldap://ldap001.contoso.com:3268</value> <value>ldap://ldap002.contoso.com:3268</value> </list> </constructor-arg> <constructor-arg index="1" value="dc=contoso,dc=com" /> <property name="userDn" value="xxxxxx@contoso.com" /> <property name="password" value="yyyyy12345" /> </bean> <!-- LDAP authentication provider --> <bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider"> ★8 <constructor-arg> <bean class="org.springframework.security.ldap.authentication.BindAuthenticator"> <constructor-arg ref="contextSource" /> <property name="userSearch" ref="userSearch" /> </bean> </constructor-arg> </bean> <!-- LDAP user search --> <bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch"> ★9 <constructor-arg index="0" value="" /> <constructor-arg index="1" value="(&(objectCategory=Person)(sAMAccountName={0}))" /> <constructor-arg index="2" ref="contextSource" /> </bean> <!-- LDAP userDetailsService --> <bean id="ldapUserDetailsService" class="org.springframework.security.ldap.userdetails.LdapUserDetailsService"> ★10 <constructor-arg ref="userSearch" /> <property name="userDetailsMapper" ref="ldapUserDetailsMapper" /> </bean> <!-- UserDetailsMapper --> <bean id="ldapUserDetailsMapper" class="org.springframework.security.ldap.userdetails.LdapUserDetailsMapper" /> <!-- SimpleUrlLogoutSuccessHandler --> <bean id="simpleUrlLogoutSuccessHandler" class="org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler"> <property name="useReferer" value="true" /> </bean> </beans>
★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; /** * <H3> * Custom implementation of SavedRequestAwareAuthenticationSuccessHandler * </H3> * @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; /** * <H3> * authentication-success-handler in remember-me * </H3> * @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 * <br> * @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" %> <form name="f" action="${pageContext.request.contextPath}/logon.do" method="POST"> <table> <tr> <td><bean:message key="label.mothers" /></td> <td><input type="text" name="username" size="9" maxlength="7"></td> </tr> <tr> <td><bean:message key="label.passWord" /></td> <td><input type="password" name="password" size="20" maxlength="25" /></td> </tr> <tr> <td>Remember Me:</td> <td><input type="checkbox" name="remember-me" /></td> </tr> <tr> <td><input name="submit" type="submit" value="<bean:message key='label.login' />" /></td> </tr> </table> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> <input type="hidden" name="CACHED_REQUEST_URL" value="${sessionScope.SPRING_SECURITY_SAVED_REQUEST.redirectUrl}" /> <input type="hidden" name="REFERER_URL" value="${header['Referer']}" /> </form>
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; /** * <H3> * Action for Remember-me Login * </H3> * @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)2)
・X-Frame-Options
・X-Content-Type-Options
・X-XSS-Protection
・Strict-Transport-Security3)
デフォルト値について以下の表を参照してください。
Header name | value(default) | 目的 |
---|---|---|
Cache-Control | no-cache | コンテンツがキャッシュされるのを防ぐため |
X-Frame-Options | DENY | <iframe>に表示されるのを防ぐため |
X-Content-Type-Optionsl | nosniff | ブラウザがコンテンツの種類を判断する際、コンテンツの内容を見ないようにする |
X-Xss-Protection | 1;mode=block | XSSフィルター機能を有効にして有害なスクリプトを検知するため |
コメント