无法使用带有自定义 UserDetail 和 AuthenticationProvider 的 Spring Security 来阻止用户多次登录
Posted
技术标签:
【中文标题】无法使用带有自定义 UserDetail 和 AuthenticationProvider 的 Spring Security 来阻止用户多次登录【英文标题】:Unable to prevent user from login multiple time using spring security with custom UserDetail and AuthenticationProvider 【发布时间】:2015-09-10 10:34:04 【问题描述】:我已经实现了自定义 UserDetail 和 AuthenticationProvider 以与需要多个参数的自定义基本登录表单一起使用。一切正常,除了并发控制不会阻止用户多次登录或使用多个设备。
这是我正在使用的配置:
web.xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>kh.com.gfam.rsos.listener.InitializeApplicationListner</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<multipart-config>
<max-file-size>10485760</max-file-size>
<max-request-size>104857600</max-request-size>
<file-size-threshold>20971520</file-size-threshold>
</multipart-config>
</servlet>
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/classes/log4j.properties</param-value>
</context-param>
<context-param>
<param-name>webAppRootKey</param-name>
<param-value>rsos.root</param-value>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.png</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet>
<description></description>
<display-name>GetImageController</display-name>
<servlet-name>GetImageController</servlet-name>
<servlet-class>kh.com.gfam.rsos.presentation.controller.GetImage.GetImageController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>GetImageController</servlet-name>
<url-pattern>/GetImageController</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>home</welcome-file>
</welcome-file-list>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
spring-security.xml
<http auto-config="false" use-expressions="true"
entry-point-ref="loginUrlAuthenticationEntryPoint">
<!-- omitted -->
<intercept-url pattern="/Admin/**" access="hasRole('ROLE_ADMIN')" />
<intercept-url pattern="/Concierge/**" access="hasRole('ROLE_USER')" />
<intercept-url pattern="/Login" access="permitAll" />
<intercept-url pattern="/**" access="permitAll" />
<custom-filter position="FORM_LOGIN_FILTER"
ref="companyIdUsernamePasswordAuthenticationFilter" />
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<logout logout-url="/Logout" delete-cookies="true"
invalidate-session="true" success-handler-ref="RsosLogoutSuccessHandler" />
<csrf disabled="true" />
<session-management invalid-session-url="/Login"
session-authentication-strategy-ref="sas"/>
</http>
<global-method-security secured-annotations="enabled"/>
<authentication-manager alias="authenticationManager">
<authentication-provider
ref="companyIdUsernamePasswordAuthenticationProvider" />
</authentication-manager>
spring-config.xml
<bean id="companyIdUsernamePasswordAuthenticationProvider"
class="kh.com.gfam.rsos.common.security.RsosAuthenticationProvider" />
<bean id="companyIdUsernamePasswordAuthenticationFilter"
class="kh.com.gfam.rsos.common.security.RsosUsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="sessionAuthenticationStrategy" ref="sas" />
<property name="authenticationFailureHandler" ref="authenticationFailureHandler" />
<property name="authenticationSuccessHandler" ref="authenticationSuccessHandler" />
<property name="filterProcessesUrl" value="/Authenticate" />
</bean>
<bean id="loginUrlAuthenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<constructor-arg value="/Login" />
</bean>
<bean id="authenticationFailureHandler"
class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="/Login/defaultError" />
<property name="exceptionMappings">
<props>
<prop
key="org.springframework.security.authentication.BadCredentialsException">
/Login/badCredentials
</prop>
<prop
key="org.springframework.security.core.userdetails.UsernameNotFoundException">
/Login/usernameNotFound
</prop>
<prop
key="org.springframework.security.authentication.DisabledException">
/Login/disabled
</prop>
<prop
key="org.springframework.security.authentication.ProviderNotFoundException">
/Login/providerNotFound
</prop>
<prop
key="org.springframework.security.authentication.AuthenticationServiceException">
/Login/authenticationService
</prop>
</props>
</property>
</bean>
<bean id="authenticationSuccessHandler"
class="kh.com.gfam.rsos.common.security.RsosAuthenticationSuccessHandler">
</bean>
<bean id="RsosLogoutSuccessHandler"
class="kh.com.gfam.rsos.common.security.RsosLogoutSucessHandler"></bean>
<bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<constructor-arg name="expiredUrl" value="/session-expired.jsp" />
</bean>
<bean id="sas"
class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<property name="maximumSessions" value="1" />
<property name="exceptionIfMaximumExceeded" value="true" />
</bean>
<bean id="sessionRegistry"
class="org.springframework.security.core.session.SessionRegistryImpl"/>
根上下文.xml
<import resource="classpath:springConfig.xml" />
<import resource="appServlet/servlet-context.xml" />
<import resource="appServlet/spring-security.xml" />
<bean id="InitializationService"
class="kh.com.gfam.rsos.businesslogic.initialization.impl.InitializationServiceImpl">
</bean>
自定义 AuthenticationProvider 类
public class RsosAuthenticationProvider implements AuthenticationProvider
@Autowired
LoginService service;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException
RsosUsernamePasswordAuthenticationToken auth = (RsosUsernamePasswordAuthenticationToken) authentication;
String user_id = String.valueOf(auth.getName());
String password = String.valueOf(auth.getCredentials());
int hotel_code = auth.getHotel_code();
int user_type = auth.getUser_type();
UserDTO user = null;
user = service.authenicate(hotel_code, user_id, password, user_type);
if (user.getUser_type() == 1)
return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
else
return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN")));
@Override
public boolean supports(Class<?> authentication)
return RsosUsernamePasswordAuthenticationToken.class
.equals(authentication);
自定义用户名密码认证令牌
public class RsosUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken
private static final long serialVersionUID = 9103166337373681531L;
private final int hotel_code;
private final int user_type;
public RsosUsernamePasswordAuthenticationToken(Object principal, Object credentials,
int hotel_code, int user_type)
super(principal, credentials);
this.hotel_code = hotel_code;
this.user_type = user_type;
public RsosUsernamePasswordAuthenticationToken(Object principal, Object credentials,
int hotel_code, int user_type,
Collection<? extends GrantedAuthority> authorities)
super(principal, credentials, authorities);
this.hotel_code = hotel_code;
this.user_type = user_type;
public int getHotel_code()
return hotel_code;
public int getUser_type()
return user_type;
自定义用户名密码验证过滤器
public class RsosUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException
if (!request.getMethod().equals("POST"))
throw new AuthenticationServiceException("Authentication method not supported:"
+ request.getMethod());
String username = super.obtainUsername(request);
String password = super.obtainPassword(request);
int hotel_code = Integer.parseInt(ObtainHotelCode(request));
int user_type = Integer.parseInt(ObtainUserType(request));
if (!StringUtils.hasText(hotel_code + ""))
throw new AuthenticationServiceException("Hotel Code is require");
RsosUsernamePasswordAuthenticationToken authrequest =
new RsosUsernamePasswordAuthenticationToken(username, password, hotel_code, user_type); // (1)
setDetails(request, authrequest);
return this.getAuthenticationManager().authenticate(authrequest);
protected String ObtainHotelCode(HttpServletRequest request)
return request.getParameter("hotel_code");
protected String ObtainUserType(HttpServletRequest request)
return request.getParameter("user_type");
自定义 AuthenticationSuccessHandler
public class RsosAuthenticationSuccessHandler implements AuthenticationSuccessHandler
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException
UserDTO authUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication()
.getPrincipal();
request.getSession().setAttribute("userData", authUser);
response.setStatus(HttpServletResponse.SC_OK);
if (authUser.getUser_type() == 1)
response.sendRedirect(request.getContextPath() + "/Concierge/New_Arrival");
else
response.sendRedirect(request.getContextPath() + "/Admin/Main_Info");
自定义 LogoutSucessHandler
public class RsosLogoutSucessHandler implements LogoutSuccessHandler
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException
if (authentication != null && authentication.getDetails() != null)
try
request.getSession().invalidate();
catch (Exception e)
e.printStackTrace();
e = null;
response.setStatus(HttpServletResponse.SC_OK);
response.sendRedirect(request.getContextPath() + "/Login");
自定义 UserDetail 类
public class UserDTO implements UserDetails
/** */
private static final long serialVersionUID = -2228367483835088451L;
/** Hotel Code */
private int hotel_code;
/** UserID */
// @Size(min=4, max=4) @Pattern(regexp = "[0-9]")
private String user_id;
/** User Name */
private String user_name;
/** Password */
@NotNull
@Size(min = 8, max = 30)
private String password;
/** User Type */
@NotNull
private int user_type;
//set() .... get()//
public void setUser_name(String user_name)
this.user_name = user_name;
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
return null;
@Override
public String getUsername()
return user_id;
@Override
public boolean isAccountNonExpired()
return false;
@Override
public boolean isAccountNonLocked()
return false;
@Override
public boolean isCredentialsNonExpired()
return false;
@Override
public boolean isEnabled()
return false;
@Override
public boolean equals(Object rhs)
if (rhs instanceof UserDTO)
return (user_id.equals(((UserDTO) rhs).user_id));
return false;
/**
* Returns the hashcode of the @code username.
*/
@Override
public int hashCode()
return user_id.hashCode();
这就是登录表单的样子
我是 Spring Security 的新手,所以这里可能出现什么问题?有人可以指出错误吗? 谢谢。
【问题讨论】:
试试这个,使用这个类org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy
, <bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy"> <constructor-arg name="sessionRegistry" ref="sessionRegistry" /> <property name="maximumSessions" value="1" /> <property name="exceptionIfMaximumExceeded" value="true" /> </bean>
我使用的是 Spring Security 4.0.1,似乎这个类被替换为 org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy
【参考方案1】:
在阅读了这里http://docs.spring.io/spring-security/site/migrate/current/3-to-4/html5/migrate-3-to-4-xml.html#m3to4-deprecations-web-cscs中关于如何从 Spring 3.2 迁移到 4.x 的 Spring 文档后,我已经解决了这个问题
我变了
<bean id="sas"
class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<property name="maximumSessions" value="1" />
<property name="exceptionIfMaximumExceeded" value="true" />
</bean>
到
<bean id="sas"
class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<constructor-arg>
<list>
<bean
class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<constructor-arg ref="sessionRegistry" />
<property name="maximumSessions" value="1" />
<property name="exceptionIfMaximumExceeded" value="true" />
</bean>
<bean
class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
<bean
class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
<constructor-arg ref="sessionRegistry" />
</bean>
</list>
</constructor-arg>
</bean>
对于那些使用 Spring 4.x 并遇到同样问题的人,您可以尝试阅读上面链接中的整个文档,它可能也会解决您的问题。
【讨论】:
【参考方案2】:除了现有的会话策略之外,您似乎还缺少 ConcurrentSessionControlStrategy。如果您使用的是 spring 3.2,您可以创建一个 complexSessionStrategy 并将两者捆绑到一个策略中以作为您的参考传递
【讨论】:
没有。我看到有人建议覆盖我已经做过的 UserDetail 的 equals() 和 hashCode() 方法,但似乎 SessionRegistryImpl 的方法 registerNewSession() 根本没有被调用。我是否也需要自定义实现 SessionRegistry? @light_303 我目前正在使用 Spring 4.0.1 并且该类不再存在,它已被 ConcurrentSessionControlAuthenticationStrategy 取代,您能否给我更多有关如何创建复合会话策略的详细信息?我是新手,所以我完全不知道从哪里开始。以上是关于无法使用带有自定义 UserDetail 和 AuthenticationProvider 的 Spring Security 来阻止用户多次登录的主要内容,如果未能解决你的问题,请参考以下文章
带有自定义控件的视频无法在 iOS 上播放(Safari 和 Chrome)
无法使用带有自定义单元的 BaseButtonBarPagerTabStripViewController 连接 XLPagertabStrip 插座