Spring Security应用开发(09)密码错误次数限制

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security应用开发(09)密码错误次数限制相关的知识,希望对你有一定的参考价值。

实现登录时密码错误次数限制功能,就是在登录界面中当用户提交了错误的密码时在数据库中记录下这个错误次数,直到错误次数达到指定次数时,锁定用户账户,此时即便输入正确的密码,也不能登录。

需要完成如下工作:

(1)修改用户表users的结构,增加相关字段。

(2)自定义实现UserDetailsService,用于加载额外的数据字段。

(3)自定义实现AuthenticationProvider,用于捕获登录成功和失败的事件。

(3)修改spring-security.xml文件,配置上述(2)(3)的信息。

(4)修改登录失败页面,显示具体登录错误信息。

 

1.1.1. 修改用户表结构

users表的表结构做如下修改,

增加四个字段:

账户是否过期: expired

账户是否锁定:locked

密码是否过期:passwordexpired

登录失败次数:failtimes

Spring SecurityUserDetails接口以及User类中均定义了前3个字段对应的属性,但是在查询数据库时,默认没有查询这三个字段,在创建User实例时均以true进行构造。在自定义UserDetailsService时将仿照JdbcDaoImplloadUsersByUsername()方法进行改造。

 

 

具体SQL操作如下:

 

mysql> alter table users add column expired boolean not null;

Query OK, 0 rows affected (0.28 sec)

Records: 0  Duplicates: 0  Warnings: 0

 

mysql> alter table users add column locked boolean not null;

Query OK, 0 rows affected (0.06 sec)

Records: 0  Duplicates: 0  Warnings: 0

 

mysql> alter table users add column passwordexpired boolean not null;

Query OK, 0 rows affected (0.08 sec)

Records: 0  Duplicates: 0  Warnings: 0

 

mysql> alter table users add column failtimes int not null default 0;

Query OK, 0 rows affected (0.34 sec)

Records: 0  Duplicates: 0  Warnings: 0

 

mysql> desc users;

+-----------------+-------------+------+-----+---------+-------+

| Field           | Type        | Null | Key | Default | Extra |

+-----------------+-------------+------+-----+---------+-------+

| username        | varchar(64) | NO   | PRI | NULL    |       |

| password        | varchar(64) | NO   |     | NULL    |       |

| enabled         | tinyint(1)  | NO   |     | NULL    |       |

| expired         | tinyint(1)  | NO   |     | NULL    |       |

| locked          | tinyint(1)  | NO   |     | NULL    |       |

| passwordexpired | tinyint(1)  | NO   |     | NULL    |       |

| failtimes       | int(11)     | NO   |     | 0       |       |

+-----------------+-------------+------+-----+---------+-------+

7 rows in set (0.00 sec)

 

mysql> select * from users;

+----------+------------------------------------------+---------+---------+--------

 

+-----------------+-----------+

| username | password                                 | enabled | expired | locked |

 

passwordexpired | failtimes |

+----------+------------------------------------------+---------+---------+--------

 

+-----------------+-----------+

| lisi     | 40bd001563085fc35165329ea1ff5c5ecbdbbeef |       1 |       0 |      0 |

 

              0 |         0 |

| wangwu   | 40bd001563085fc35165329ea1ff5c5ecbdbbeef |       1 |       0 |      0 |

 

              0 |         0 |

| zhangsan | 40bd001563085fc35165329ea1ff5c5ecbdbbeef |       1 |       0 |      0 |

 

              0 |         0 |

+----------+------------------------------------------+---------+---------+--------

 

+-----------------+-----------+

3 rows in set (0.00 sec)

 

 

1.1.2. 实现自定义的UserDetailsService

 

(1)先定义一个UserDetailsUpdater接口。

此接口类型将作为CustomAuthenticationProvider的登录辅助信息维护对象CustomUserDetailsService的接口类型。

 

/**

* @ClassName: UserDetailsUpdater

* @Description: 用于维护用户登录辅助信息

* @author http://www.cnblogs.com/coe2coe/

*  

*/

public interface UserDetailsUpdater {

 

/**

 * 在登录密码错误和登录成功时维护登录辅助信息。

 * @param username  用户名

 * @param success   登录是否成功

 * @throws Exception

 */

void updateUser(String username, boolean success) throws Exception;


}

 

 

 

(2)定义自定义的CustomUserDetailsService类。

Spring SecurityJdbcDaoImpl类继承,同时实现了UserDetailsUpdater接口。

 

/**

* @ClassName: CustomUserDetailsService

* @Description: (1)从数据库中加载安全相关的用户信息,添加了SpringSecurity默认不包含的3个字段。

*               (2)实现UserDetailsUpdater,维护登录辅助信息。

* @author http://www.cnblogs.com/coe2coe/

*  

*/

public class CustomUserDetailsService extends JdbcDaoImpl implements UserDetailsUpdater {

 

/**

 * 从数据库查询用户信息。

 */

@Override

protected List<UserDetails> loadUsersByUsername(String username) {

return getJdbcTemplate().query(this.getUsersByUsernameQuery(),

new String[] { username }, new RowMapper<UserDetails>() {

@Override

public UserDetails mapRow(ResultSet rs, int rowNum)

throws SQLException {

String username = rs.getString("username");

String password = rs.getString("password");

boolean enabled = rs.getBoolean("enabled");

boolean locked = rs.getBoolean("locked");

boolean expired = rs.getBoolean("expired");

boolean passwordExpired = rs.getBoolean("passwordexpired");

return new User(username, password, enabled,

!expired,!passwordExpired,!locked,

AuthorityUtils.NO_AUTHORITIES);

}

 

});

}

 

/**

 * 主要作用是使SpringSecurity最终使用的UserDetails不必要与从数据库查询出的UserDetails完全相同。

 * 提供了一个间接的中间层。

 */

@Override

protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery,

List<GrantedAuthority> combinedAuthorities) {

String returnUsername = userFromUserQuery.getUsername();

 

if (!this.isUsernameBasedPrimaryKey()) {

returnUsername = username;

}

return new User(returnUsername,

userFromUserQuery.getPassword(),

userFromUserQuery.isEnabled(),

userFromUserQuery.isAccountNonExpired(),

userFromUserQuery.isCredentialsNonExpired(),

userFromUserQuery.isAccountNonLocked(),

combinedAuthorities);

}

 

 

@Override

public void updateUser(String username, boolean success) throws Exception {

if(success){

this.getJdbcTemplate().update(sqlUnlockUser, username);

}

else{

this.getJdbcTemplate().update(sqlIncreaseFailTimes,username);

if(this.getJdbcTemplate().queryForObject(sqlQueryFailTimes, Integer.class,username) >= maxFailTimesBeforeLock)

{

this.getJdbcTemplate().update(sqlLockUser,username);

}

}

}

 

//最大的失败次数

private int     maxFailTimesBeforeLock = 5;

 

//解锁账户

private String  sqlUnlockUser = "update users set locked = false,failtimes=0 where username=?";

 

//锁定账户

private String  sqlLockUser = "update users set locked = true where username=? and locked = false";

//增加失败次数

private String  sqlIncreaseFailTimes = "update users set failtimes = failtimes + 1 where username=?";

 

//查询失败次数。

private String  sqlQueryFailTimes = "select failtimes from users where username=?";

 

 

public String getSqlUnlockUser() {

return sqlUnlockUser;

}

public void setSqlUnlockUser(String sqlUnlockUser) {

this.sqlUnlockUser = sqlUnlockUser;

}

public String getSqlLockUser() {

return sqlLockUser;

}

public void setSqlLockUser(String sqlLockUser) {

this.sqlLockUser = sqlLockUser;

}

public String getSqlIncreaseFailTimes() {

return sqlIncreaseFailTimes;

}

public void setSqlIncreaseFailTimes(String sqlIncreaseFailTimes) {

this.sqlIncreaseFailTimes = sqlIncreaseFailTimes;

}

}

 

 

 

 

 

自定义CustomAuthenticationProvider

 

主要作用是在用户登录时出现密码错误时以及登录成功时进行自定义的处理。

 

/**

* @ClassName: CustomAuthenticationProvider

* @Description: 自定义的一个用户认证提供者。

* @author http://www.cnblogs.com/coe2coe/

*  

*/

public class CustomAuthenticationProvider extends DaoAuthenticationProvider {

 

@Override

public Authentication authenticate(Authentication auth) throws AuthenticationException {

System.out.println("authenticate begin------");

Authentication  authResult = null;

try{

authResult =  super.authenticate(auth);

 

try{//验证成功,重置密码错误次数。

this.userDetailsUpdater.updateUser(auth.getName(), true);

}

catch(Exception exp){

exp.printStackTrace();

}

}

catch(BadCredentialsException ex){//密码错误,增加密码错误次数,达到最大次数时锁定账户。

    System.out.println("BadCredentialsException:" + auth.getName());

  try{

this.userDetailsUpdater.updateUser(auth.getName(), false);

}

catch(Exception exp){

exp.printStackTrace();

}

throw ex;

}

catch(AuthenticationException ex){

System.out.println("AuthenticationException:" + auth.getName());

System.out.println(auth.getDetails());

System.out.println(auth.getPrincipal());

   throw ex;

}

System.out.println("authenticate end--------");

return authResult;

}

 

private UserDetailsUpdater  userDetailsUpdater;

 

public UserDetailsUpdater getUserDetailsUpdater() {

return userDetailsUpdater;

}

 

public void setUserDetailsUpdater(UserDetailsUpdater userDetailsUpdater) {

this.userDetailsUpdater = userDetailsUpdater;

}

}

 

 

1.1.3. 修改spring-security.xml文件

主要目的是将上述的自定义CustomUserDetailsServiceCustomAuthenticationProvider类进行配置,并配置到AuthenticationManager中。

 

 

<!-- 用户和角色的对应关系 -->

 <sec:authentication-manager>

 <!-- 指定AuthenticationProvider为自定义的CustomAuthenticationProvider -->

   <sec:authentication-provider ref="authenticationProvider"  />

 </sec:authentication-manager>

 

 

<!-- 自定义的CustomUserDetailsService -->

<beans:bean  id="userDetailsService"  class="com.test.security.CustomUserDetailsService" >

  <beans:property name="dataSource" ref="dataSource"></beans:property>

  <beans:property name="usersByUsernameQuery"

   value="select * from users where username=?"

   ></beans:property>

</beans:bean>

 

<!-- 自定义的CustomAuthenticationProvider

     将UserDtailsService和UserDetailsUpdater注入其中。

 -->

<beans:bean id="authenticationProvider" class="com.test.security.CustomAuthenticationProvider">

 <beans:property name="userDetailsService" ref="userDetailsService"></beans:property>

 <beans:property name="passwordEncoder" ref="passwordEncoder"></beans:property>

 <beans:property name="userDetailsUpdater" ref="userDetailsService"></beans:property>

</beans:bean>

 

<!-- 仍然是使用SHA摘要算法处理密码 -->

 <beans:bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"></beans:bean>

 

 

 

 

1.1.4. 修改登录失败页面

 

目的是希望在登录失败时显示具体登录错误信息。

Spring Security将登录失败时的异常对象存放在requst对象的属性中。

login_failed.jsp中增加如下代码:

 

<p>${SPRING_SECURITY_LAST_EXCEPTION.message}</p>

  

 

登录密码错误时显示密码错误:

 技术分享

 

 

当累计5次密码错误之后,再次登录时显示账户已锁定:

 技术分享

 

 

1.1.5. 总结

有几个需要注意的地方:

  (a)使用的密码摘要算法的类名可以在Spring Security的源代码中找到。

  (b)原始的Spring Security加载用户表users中的信息时,没有加载登录辅助信息,所以进行了自定义,编写了新的加载过程。

  (c)CustomAuthenticationProviderbean定义中,属性userDetailsServiceSpring SecurityDaoAuthenticationProvider所要求的;属性userDetailsUpdater是自行添加的。二者都将指向同一个bean对象userDetailsService,该bean对象直接调用JdbcTemplate的方法操纵数据库。

 

以上是关于Spring Security应用开发(09)密码错误次数限制的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security 无法识别自己的加密

spring security ldap隐藏密码属性

Spring Security 无密码编码

使用 Spring Security 为 Web 客户端授予针对 REST 服务器的 Oauth2 密码

Spring Security 自定义 UserDetails :盐问题

Spring Security 会话中的纯文本密码