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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/17848757/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-01 17:47:43  来源:igfitidea点击:

How to Enforce Change Password on User's initial login using Spring Security

javaspring-mvcspring-securitychange-password

提问by Jonathan

What would be the most elegant way of implementing a force password change upon user's initial login using Spring Security?

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

I tried implementing a custom AuthenticationSuccessHandleras mentioned here, but as mentioned by rodrigoap, if a user manually inputs the URL at the address bar, the user will still be able to proceed to that page even if he didn't change his password.

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

I did this with a filter ForceChangePasswordFilter. Because if the user types the url by hand they can bypass the change password form. With the filter the request always get intercepted.

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

As such, I proceeded with implementing a custom filter.

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

My question is this, when I implement a custom filter and send a redirect inside it, it goes through the filter again causing an infinite redirect loop as mentioned here. I tried implementing the solution mentioned by declaring two http tags in my security-context.xml with the first tag having the patternattribute as such but it still goes through my custom filter:

我的问题是,当我实现一个自定义过滤器和发送里面重定向,它会通过过滤器再次提到引起无限重定向循环这里。我尝试通过在我的 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>

What my current implementation is (which works) is:

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

  • Inside my custom authentication success handler, I set a session attribute isFirstLogin
  • In my ForcePasswordChangeFilter, I check if the session isFirstLoginis set
    • If it is, then I send a redirect to my force password change
    • Else, I call chain.doFilter()
  • 在我的自定义身份验证成功处理程序中,我设置了一个会话属性 isFirstLogin
  • 在我的 ForcePasswordChangeFilter 中,我检查会话isFirstLogin是否已设置
    • 如果是,那么我发送重定向到我的强制密码更改
    • 否则我叫 chain.doFilter()

My problem with this implementation is that access to my resources folder also goes through this filter which causes my page to be distorted (because *.js and *.css are not successfully retrieved). This is the reason I tried having two <http>tags in my security app context.xml (which didn't work).

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

As such, I ended up having to manually filter the request if the servletPath starts or contains "/resources". I didn't want it to be like this - having to manually filter the request path - but for now it's what I have.

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

What's the more elegant way of doing this?

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

回答by sezerug

I solved this issue by providing a status value for the user,

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

  • status=-1 ; initial login
  • status=0 ; deactive account
  • status=1 ; active account
  • 状态=-1 ; 初次登录
  • 状态=0;停用帐户
  • 状态=1;活跃帐户

and 2 custom authentication controller in the security.xml. First for to check username, pass and second for the additional controls like initial login, password expiration policy.

和 2 个自定义身份验证控制器在security.xml. 首先检查用户名、密码,然后检查初始登录、密码过期策略等附加控制。

In case of first login, providing correct values of username and password, first controller (user-service-ref="jdbcUserService") fails to authenticate user(because user's status=-1) than second controller(ref="myAuthenticationController") catches the request. In this controller DisabledExceptionis thrown.

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

Finally, you can redirect user to password-change page on AuthenticationFailureListener's onAuthenticationFailuremethod.

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

A part of security.xml

一部分 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));
        }
    }
}

回答by Jayesh

I am not sure such functionality is provided by spring as in-built.

我不确定这种功能是由 spring 提供的内置功能。

I have achieved similar things using setting one column in table which help me identify first time login by user or not.

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

If it is first time login then view to display in my case was reset Password page otherwise my dashboard page.

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

回答by Mahdi

in ForceChangePasswordFilter, for stopping filter from loop you should check ServletPath containing ChangePassword url or not. like this:

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

    if(multiReadRequest.getServletPath().startsWith("/ChangePass.htm"))
       flag=false;

回答by Ondrej Burkert

I had a same problem with resources. I went for following:

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

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

I had these set as sec:intercept-url with access="IS_AUTHENTICATED_ANONYMOUSLY which is ultimately same thing as not securing the access to those resources by Spring Security.

我将这些设置为 sec:intercept-url with access="IS_AUTHENTICATED_ANONYMOUSLY 这与没有通过 Spring Security 保护对这些资源的访问最终相同。

As for the 'infinite redirect loop' problem you solved by having some firstLogin variable, I solved that by merely comparing the request URL with the password change URL I would redirect to and it worked. Though I would be happy to hear if I overlooked something.

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

回答by rajasekaran

I handle password modification like the modification of any other entity field.

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

In this situation you can create an update form for an hypothetic user object. When you save your user entity in the database you may need to save the hashed password, handle salt etc. But this is not a Spring security job.

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