无法使用带有自定义 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.ConcurrentSessi‌​onControlStrategy , &lt;bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy"&gt; &lt;constructor-arg name="sessionRegistry" ref="sessionRegistry" /&gt; &lt;property name="maximumSessions" value="1" /&gt; &lt;property name="exceptionIfMaximumExceeded" value="true" /&gt; &lt;/bean&gt; 我使用的是 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 来阻止用户多次登录的主要内容,如果未能解决你的问题,请参考以下文章

django - 自定义过滤器无法正常工作

带有自定义控件的视频无法在 iOS 上播放(Safari 和 Chrome)

无法使用带有自定义单元的 BaseButtonBarPagerTabStripViewController 连接 XLPagertabStrip 插座

带有复选框和自定义适配器的 ListView,片段无法正常工作

使用带有选项卡布局的自定义视图时无法从选项卡中删除填充

带有自定义视图的 CollapsingToolbarLayout