7

使用 Spring Security 在用户初始登录时实现强制密码更改的最优雅方法是什么?

我尝试实现这里AuthenticationSuccessHandler提到的自定义,但正如rodrigoap提到的,如果用户在地址栏手动输入 URL,即使他没有更改密码,用户仍然可以继续访问该页面。

我用过滤器 ForceChangePasswordFilter 做到了这一点。因为如果用户手动输入 url,他们可以绕过更改密码表单。使用过滤器,请求总是被拦截。

因此,我继续实施自定义过滤器。

我的问题是,当我实现自定义过滤器并在其中发送重定向时,它再次通过过滤器导致无限重定向循环,如此所述。我尝试通过在我的security-context.xml中声明两个http标签来实现提到的解决方案,第一个标签具有这样的pattern属性,但它仍然通过我的自定义过滤器:

<http pattern="/resources" security="none"/>
<http use-expressions="true" once-per-request="false"
    auto-config="true">
  <intercept-url pattern="/soapServices/**" access="permitAll" requires-channel="https"/>
  ...
  <custom-filter position="LAST" ref="passwordChangeFilter" />
</http>
...
<beans:bean id="passwordChangeFilter"
  class="my.package.ForcePasswordChangeFilter"/>
<beans:bean id="customAuthenticationSuccessHandler"
  class="my.package.CustomAuthenticationSuccessHandler" >
</beans:bean>
<beans:bean id="customAuthenticationFailureHandler"
  class="my.package.CustomAuthenticationFailureHandler" >
  <beans:property name="defaultFailureUrl" value="/login"/>
</beans:bean>

我目前的实现是(有效的)是:

  • 在我的自定义身份验证成功处理程序中,我设置了一个会话属性isFirstLogin
  • 在我的 ForcePasswordChangeFilter 中,我检查是否isFirstLogin设置 了会话
    • 如果是,那么我发送重定向到我的强制密码更改
    • 否则,我打电话chain.doFilter()

我对这个实现的问题是对我的资源文件夹的访问也会通过这个过滤器,这会导致我的页面被扭曲(因为 *.js 和 *.css 没有成功检索)。这就是我尝试<http>在我的安全应用程序 context.xml 中包含两个标签的原因(这不起作用)。

因此,如果 servletPath 启动或包含“/resources”,我最终不得不手动过滤请求。我不希望它是这样的——不得不手动过滤请求路径——但现在它就是我所拥有的。

这样做更优雅的方式是什么?

4

5 回答 5

10

我通过为用户提供状态值解决了这个问题,

  • 状态=-1 ; 初始登录
  • 状态=0;停用帐户
  • 状态=1;活跃账户

和 2 个自定义身份验证控制器security.xml。首先是检查用户名,通过,其次是额外的控制,如初始登录,密码过期策略。

在第一次登录的情况下,提供正确的用户名和密码值,第一个控制器 ( user-service-ref="jdbcUserService") 无法验证用户身份(因为用户的 status=-1)比第二个控制器 ( ref="myAuthenticationController") 捕获请求。在这个控制器DisabledException中被抛出。

AuthenticationFailureListener最后,您可以将用户重定向到'onAuthenticationFailure方法上的密码更改页面。

一部分security.xml

<authentication-manager alias="authenticationManager">
    <authentication-provider user-service-ref="jdbcUserService">
        <password-encoder ref="passwordEncoder" />
    </authentication-provider>
    <authentication-provider ref="myAuthenticationController" />
</authentication-manager>

<beans:bean id="jdbcUserService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
    <beans:property name="rolePrefix" value="ROLE_" />
    <beans:property name="dataSource" ref="dataSource" />
    <beans:property name="usersByUsernameQuery" value="SELECT user_name as userName, PASSWORD as password, STATUS as status FROM  USER WHERE  user_name = ? AND STATUS=1" />
    <beans:property name="authoritiesByUsernameQuery" value="SELECT user_name as userName, ROLE as authority FROM USER WHERE user_name = ?" />
</beans:bean>

<beans:bean id="myAuthenticationController" class="com.test.myAuthenticationController">
    <beans:property name="adminUser" value="admin" />
    <beans:property name="adminPassword" value="admin" />
</beans:bean>

<!--Custom authentication success handler for logging/locking/redirecting-->

<beans:bean id="authSuccessHandler" class="com.test.AuthenticationSuccessListener"/>

<!--Custom authentication failure handler for logging/locking/redirecting-->

<beans:bean id="authFailureHandler" class="com.test.AuthenticationFailureListener"/>

@Service("myAuthenticationController")
public class MyAuthenticationController extends AbstractUserDetailsAuthenticationProvider {

    private final Logger logger = Logger.getLogger(getClass());

    @Autowired
    private WfmUserValidator userValidator;
    private String username;
    private String password;

    @Required
    public void setAdminUser(String username) {
        this.username = username;
    }

    @Required
    public void setAdminPassword(String password) {
        this.password = password;
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        return;
    }

    @Override
    protected UserDetails retrieveUser(String userName, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        String password = (String) authentication.getCredentials();
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        String userRole = "";


        if (status = -1) {
            throw new DisabledException("It is first login. Password change is required!");
        } else if (password expired) {
            throw new CredentialsExpiredException("Password is expired. Please change it!");
        }

        return new User(userName, password, true, // enabled
                true, // account not expired
                true, // credentials not expired
                true, // account not locked
                authorities);
    }
}

public class AuthenticationFailureListener implements AuthenticationFailureHandler {

    private static Logger logger = Logger.getLogger(AuthenticationFailureListener.class);
    private static final String BAD_CREDENTIALS_MESSAGE = "bad_credentials_message";
    private static final String CREDENTIALS_EXPIRED_MESSAGE = "credentials_expired_message";
    private static final String DISABLED_MESSAGE = "disabled_message";
    private static final String LOCKED_MESSAGE = "locked_message";

    @Override
    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException ex) throws IOException, ServletException {
        // TODO Auto-generated method stub
        String userName = req.getParameter("j_username");
        logger.info("[AuthenticationFailure]:" + " [Username]:" + userName + " [Error message]:" + ex.getMessage());

        if (ex instanceof BadCredentialsException) {
            res.sendRedirect("../pages/login.jsf?message=" + MessageFactory.getMessageValue(BAD_CREDENTIALS_MESSAGE));
        } else if (ex instanceof CredentialsExpiredException) {
            res.sendRedirect("../pages/changecredentials.jsf?message=" + MessageFactory.getMessageValue(CREDENTIALS_EXPIRED_MESSAGE));
        } else if (ex instanceof DisabledException) {
            res.sendRedirect("../pages/changecredentials.jsf?message=" + MessageFactory.getMessageValue(DISABLED_MESSAGE));
        } else if (ex instanceof LockedException) {
            res.sendRedirect("../pages/login.jsf?message=" + MessageFactory.getMessageValue(LOCKED_MESSAGE));
        }
    }
}
于 2013-07-29T11:42:26.610 回答
6

我不确定这样的功能是由 spring 提供的。

我已经通过在表中设置一列来实现类似的事情,这有助于我识别用户是否首次登录。

如果是第一次登录,那么在我的情况下要显示的视图是重置密码页面,否则是我的仪表板页面。

于 2013-07-25T04:11:58.797 回答
1

在 ForceChangePasswordFilter 中,要从循环中停止过滤器,您应该检查是否包含 ChangePassword url 的 ServletPath。像这样:

    if(multiReadRequest.getServletPath().startsWith("/ChangePass.htm"))
       flag=false;
于 2014-03-17T07:33:11.473 回答
0

我在资源方面遇到了同样的问题。我去了以下:

<sec:http pattern="/css/**" security="none" />
<sec:http pattern="/favicon.ico" security="none" />
<sec:http pattern="/wicket/resource/**" security="none" />

我将这些设置为 sec:intercept-url 和 access="IS_AUTHENTICATED_ANONYMOUSLY,这最终与 Spring Security 不保护对这些资源的访问是一样的。

至于您通过使用一些 firstLogin 变量解决的“无限重定向循环”问题,我仅通过将请求 URL 与我将重定向到的密码更改 URL 进行比较来解决该问题,并且它起作用了。尽管如果我忽略了某些事情,我会很高兴听到。

于 2014-04-30T08:54:13.510 回答
-2

我处理密码修改就像修改任何其他实体字段一样。

在这种情况下,您可以为假设的用户对象创建更新表单。当您将用户实体保存在数据库中时,您可能需要保存散列密码、处理盐等。但这不是 Spring 安全工作。

于 2015-10-03T13:42:31.827 回答