如何使用Spring Security在用户初始登录时强制更改密码

Jon*_*han 5 java spring-mvc change-password spring-security

在用户使用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>
Run Code Online (Sandbox Code Playgroud)

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

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

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

因此,如果servletPath启动或包含"/ resources",我最终必须手动过滤请求.我不希望它像这样 - 必须手动过滤请求路径 - 但现在它就是我拥有的.

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

sez*_*rug 9

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

  • status = -1; 初次登录
  • status = 0; 不活跃的帐户
  • status = 1; 活跃帐户

和2中的自定义身份验证控制器security.xml.首先检查用户名,传递和秒,以获取其他控件,如初始登录,密码过期策略.

如果首次登录,提供正确的用户名和密码值,则第一个控制器(user-service-ref="jdbcUserService")无法验证用户(因为用户 status=-1)而非第二个控制器(ref="myAuthenticationController")捕获请求.在这个控制器DisabledException被抛出.

最后,您可以将用户重定向到密码更改页面AuthenticationFailureListeneronAuthenticationFailure方法.

一部分 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"/>
Run Code Online (Sandbox Code Playgroud)
@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);
    }
}
Run Code Online (Sandbox Code Playgroud)
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));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这不是一个安全漏洞吗?其他人(攻击者)可能尝试使用虚假凭据登录,如果用户应更改其密码,则攻击者可以更改尚未登录的用户的密码.当然,您必须在更改时提供当前密码.但这意味着更改密码端点会受到字典攻击,除非得到适当保护.这意味着攻击面更大 - 正常登录表单和更改密码表单. (3认同)

Jay*_*esh 6

我不确定弹簧是否提供内置功能.

我通过在表格中设置一列来帮助我识别用户是否第一次登录,从而实现了类似的功能.

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