Spring Security 3.1 と LDAP が統合されたアプリケーションがあります。以下は、これまでの要件と実装における重要なポイントです。
- アプリケーションは 1 人のユーザーに対して複数の役割を持ちますが、これらの役割は LDAP には存在しないため、アプリケーションは LDAP からのユーザー名 (またはユーザー ID) のみを認証します。
- ロールはデータベースに個別に保存されます
- LDAP からの認証が成功すると、UserDetailsService を実装することにより、userdetails とロールがプリンシパル オブジェクトのカスタム userdetails オブジェクトに設定されます。
問題:
- ユーザー A がアプリケーションにログインする
- ユーザー B がアプリケーションにログインすると、ユーザー A のセッションが破棄されます (ユーザー A がまだログアウトしていないため、これは発生しないはずです!)
- ユーザー B がログアウトすると、ユーザー A は、ユーザー B がログインしたときにセッションが既に破棄されているため、ページが見つからないことを取得します。
applicationContext-security.xml は次のようになります。
<beans:bean id="loginUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<beans:property name="loginFormUrl" value="/login.jsp" />
<beans:property name="forceHttps" value="true" />
</beans:bean>
<beans:bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:property name="sessionRegistry" ref="sessionRegistry" />
<beans:property name="expiredUrl" value="/login.jsp?login_error=2" />
</beans:bean>
<beans:bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<beans:constructor-arg value="/login.jsp" />
<beans:constructor-arg>
<beans:list>
<beans:ref bean="logoutEventBroadcaster" />
<beans:bean id="securityContextLogoutHandler" class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" />
</beans:list>
</beans:constructor-arg>
<beans:property name="filterProcessesUrl" value="/j_spring_security_logout" />
</beans:bean>
<beans:bean id="myAuthFilter" class="com.*.security.CustomAuthenticationProcessingFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationFailureHandler" ref="failureHandler" />
<beans:property name="authenticationSuccessHandler" ref="successHandler" />
</beans:bean>
<authentication-manager alias="authenticationManager">
<authentication-provider ref="adAuthenticationProvider" />
</authentication-manager>
<beans:bean id="adAuthenticationProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
<beans:constructor-arg value="*.*.net" />
<beans:constructor-arg value="ldap://*.*.net:389/" />
<beans:property name="userDetailsContextMapper">
<beans:bean class="com.ezadvice.service.CustomUserDetailsContextMapper" />
</beans:property>
<beans:property name="useAuthenticationRequestCredentials" value="true" />
</beans:bean>
<beans:bean id="failureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<beans:property name="defaultFailureUrl" value="/login.jsp?login_error=1" />
</beans:bean>
<beans:bean id="successHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<beans:property name="defaultTargetUrl" value="/home.do" />
<beans:property name="alwaysUseDefaultTargetUrl" value="true"/>
</beans:bean>
<beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:property name="maximumSessions" value="1" />
<beans:property name="exceptionIfMaximumExceeded" value="true" />
<beans:property name="migrateSessionAttributes" value="false" />
</beans:bean>
<beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />
CustomAuthenticationProcessingFilter クラスは次のようになります。
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String roleId = request.getParameter("roleId");
String username = request.getParameter("j_username");
TbEzaLoginHistory tbEzaLoginHistory = null;
// check if the user has authority for the role
TbEzaUser tbEzaUser = userManagementService.checkUserAndRole(roleId, username);
if (null != tbEzaUser) {
tbEzaLoginHistory = userManagementService.saveLoginHistory(tbEzaUser, roleId);
request.setAttribute("loginHistoryId", tbEzaLoginHistory.getLoginKey());
request.setAttribute("roleId", roleId);
request.setAttribute("j_username", username);
if (UserTracker.increment(username, roleId)) {
try{
Authentication attemptAuthentication = super.attemptAuthentication(request, response);
if (null != attemptAuthentication) {
CustomUser principal = (CustomUser) attemptAuthentication.getPrincipal();
if (null == principal && null != tbEzaLoginHistory)
userManagementService.deleteFromLoginHistory(tbEzaLoginHistory.getLoginKey());
return attemptAuthentication;
}
}
catch (CommunicationException e) {
userManagementService.deleteFromLoginHistory(tbEzaLoginHistory.getLoginKey());
UserTracker.decrement(username, roleId);
RequestDispatcher dispatcher = request.getRequestDispatcher("/login.jsp?login_error=5");
try {
dispatcher.forward(request, response);
} catch (ServletException se) {
se.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
}
LOGGER.debug("Connection Timeout error for UserName:"+username +"\n" + e);
e.printStackTrace();
}
}else {
if (null != tbEzaLoginHistory)
userManagementService.deleteFromLoginHistory(tbEzaLoginHistory.getLoginKey());
RequestDispatcher dispatcher = request.getRequestDispatcher("/login.jsp?login_error=4");
try {
dispatcher.forward(request, response);
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
RequestDispatcher dispatcher = request.getRequestDispatcher("/login.jsp?login_error=3");
try {
dispatcher.forward(request, response);
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(EXITLOGGER + " attemptAuthentication");
}
return null;
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, authResult);
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authResult;
WebAuthenticationDetails details = (WebAuthenticationDetails) token.getDetails();
String address = details.getRemoteAddress();
CustomUser user = (CustomUser) authResult.getPrincipal();
String userName = user.getUsername();
System.out.println("Successful login from remote address: " + address + " by username: "+ userName);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(ENTRYLOGGER + " unsuccessfulAuthentication");
}
try {
Long loginHistoryId = (Long) request.getAttribute("loginHistoryId");
String username = (String) request.getAttribute("j_username");
String roleId = (String) request.getAttribute("roleId");
userManagementService.deleteFromLoginHistory(loginHistoryId);
super.unsuccessfulAuthentication(request, response, failed);
UserTracker.decrement(username, roleId);
} catch (Exception e) {
e.printStackTrace();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(EXITLOGGER + " unsuccessfulAuthentication");
}
}
UserTracker クラスは次のようになります。
public class UserTracker {
private static Set<String> loggedInUsersDetails = new HashSet<String>();
@SuppressWarnings("unchecked")
synchronized public static boolean increment(String userName, String roleId) {
if(loggedInUsersDetails.add(userName.toLowerCase()+'~'+roleId)){
return true;
}else
return false;
}
synchronized public static void decrement(String userName, String roleId) {
loggedInUsersDetails.remove(userName.toLowerCase()+'~'+roleId);
}
ユーザーAのセッションが破壊されている理由を誰かが教えてくれますか?