====== 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" %>
===== 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|