如何在Spring Security中强制用户在首次登录时更改密码

8

如何使用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无法成功检索)。 这就是我尝试在安全应用程序context.xml中使用两个<http>标记的原因(但没有成功)。
因此,我最终不得不手动过滤请求,如果servletPath以或包含“/resources”,则进行过滤。 我不想这样做 - 不想手动过滤请求路径 - 但目前只能这样做。
有更优雅的方法吗?

1
我认为问题在于你的security=none URL模式应该是"/resources/**"而不是"/resources"。这行代码的目的是将你的资源从所有安全过滤器中排除。按照现有的写法,它只会精确匹配"/resources",而不是你的CSS和JavaScript文件。 - drrob
5个回答

10

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

  • status=-1 ; 初始登录
  • status=0 ; 停用账户
  • status=1 ; 启用账户

并在security.xml中提供了2个自定义的身份验证控制器。第一个用于检查用户名和密码,第二个用于额外的控制,如初始登录、密码过期策略。

如果是第一次登录,并且提供了正确的用户名和密码,第一个控制器(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"/>

@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));
        }
    }
}

3
这不是一个安全漏洞吗?其他人(攻击者)可能会试图使用虚假凭据登录,如果用户更改密码,那么攻击者就可以更改尚未登录的用户的密码。当然,在更改密码时必须提供当前密码。但这意味着更改密码端点容易受到字典攻击的影响,除非得到适当保护。这意味着攻击面更大——普通登录表格和更改密码表格。 - Andrei Epure is hiring

6

我不确定Spring框架内置了这样的功能。

我通过设置表中的一列来实现类似的功能,用于确定用户是否首次登录。

如果是首次登录,则在我的情况下显示重置密码页面,否则显示我的仪表板页面。


1
在ForceChangePasswordFilter中,为了防止过滤器循环,您应该检查ServletPath是否包含ChangePassword URL。就像这样:
    if(multiReadRequest.getServletPath().startsWith("/ChangePass.htm"))
       flag=false;

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来解决的。虽然如果我有所忽略,我会很高兴听到你的意见。


-2

我处理密码修改的方式就像处理其他实体字段一样。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接