实现登录时密码错误次数限制功能,就是在登录界面中当用户提交了错误的密码时在数据库中记录下这个错误次数,直到错误次数达到指定次数时,锁定用户账户,此时即便输入正确的密码,也不能登录。
需要完成如下工作:
(1)修改用户表users的结构,增加相关字段。
(2)自定义实现UserDetailsService,用于加载额外的数据字段。
(3)自定义实现AuthenticationProvider,用于捕获登录成功和失败的事件。
(3)修改spring-security.xml文件,配置上述(2)和(3)的信息。
(4)修改登录失败页面,显示具体登录错误信息。
1.1.1. 修改用户表结构
对users表的表结构做如下修改,
增加四个字段:
账户是否过期: expired
账户是否锁定:locked
密码是否过期:passwordexpired
登录失败次数:failtimes
Spring Security在UserDetails接口以及User类中均定义了前3个字段对应的属性,但是在查询数据库时,默认没有查询这三个字段,在创建User实例时均以true进行构造。在自定义UserDetailsService时将仿照JdbcDaoImpl对loadUsersByUsername()方法进行改造。
具体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 Security的JdbcDaoImpl类继承,同时实现了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文件
主要目的是将上述的自定义CustomUserDetailsService和CustomAuthenticationProvider类进行配置,并配置到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)CustomAuthenticationProvider的bean定义中,属性userDetailsService是Spring Security的DaoAuthenticationProvider所要求的;属性userDetailsUpdater是自行添加的。二者都将指向同一个bean对象userDetailsService,该bean对象直接调用JdbcTemplate的方法操纵数据库。