今天在前面一节的基础之上,再增加一点新内容,默认情况下Spring Security不会对登录错误的尝试次数做限制,也就是说允许暴力尝试,这显然不够安全,下面的内容将带着大家一起学习如何限制登录尝试次数。
首先对之前创建的数据库表做点小调整
一、表结构调整
T_USERS增加了如下3个字段:
D_ACCOUNTNONEXPIRED,NUMBER(1) -- 表示帐号是否未过期
D_ACCOUNTNONLOCKED,NUMBER(1), -- 表示帐号是否未锁定
D_CREDENTIALSNONEXPIRED,NUMBER(1) --表示登录凭据是否未过期
要实现登录次数的限制,其实起作用的字段是D_ACCOUNTNONLOCKED,值为1时,表示正常,为0时表示被锁定,另外二个字段的作用以后的学习内容会详细解释。
新增一张表T_USER_ATTEMPTS,用来辅助记录每个用户登录错误时的尝试次数
D_ID 是流水号
D_USERNAME 用户名,外建引用T_USERS中的D_USERNAME
D_ATTEMPTS 登录次数
D_LASTMODIFIED 最后登录错误的日期
二、创建Model/DAO/DAOImpl
要对新加的T_USER_ATTEMPTS读写数据,得有一些操作DB的类,这里我们采用Spring的JDBCTemplate来处理,包结构参考下图:
T_USER_ATTEMPTS表对应的Model如下
1 package com.cnblogs.yjmyzz.model; 2 3 import java.util.Date; 4 5 public class UserAttempts { 6 7 private int id; 8 9 private String username; 10 private int attempts; 11 private Date lastModified; 12 13 public int getId() { 14 return id; 15 } 16 17 public void setId(int id) { 18 this.id = id; 19 } 20 21 public String getUsername() { 22 return username; 23 } 24 25 public void setUsername(String username) { 26 this.username = username; 27 } 28 29 public int getAttempts() { 30 return attempts; 31 } 32 33 public void setAttempts(int attempts) { 34 this.attempts = attempts; 35 } 36 37 public Date getLastModified() { 38 return lastModified; 39 } 40 41 public void setLastModified(Date lastModified) { 42 this.lastModified = lastModified; 43 } 44 45 }
对应的DAO接口
1 package com.cnblogs.yjmyzz.dao; 2 3 import com.cnblogs.yjmyzz.model.UserAttempts; 4 5 public interface UserDetailsDao { 6 7 void updateFailAttempts(String username); 8 9 void resetFailAttempts(String username); 10 11 UserAttempts getUserAttempts(String username); 12 13 }
以及DAO接口的实现
1 package com.cnblogs.yjmyzz.dao.impl; 2 3 import java.sql.ResultSet; 4 import java.sql.SQLException; 5 import java.util.Date; 6 7 import javax.annotation.PostConstruct; 8 import javax.sql.DataSource; 9 10 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.dao.EmptyResultDataAccessException; 12 import org.springframework.jdbc.core.RowMapper; 13 import org.springframework.jdbc.core.support.JdbcDaoSupport; 14 import org.springframework.stereotype.Repository; 15 import org.springframework.security.authentication.LockedException; 16 import com.cnblogs.yjmyzz.dao.UserDetailsDao; 17 import com.cnblogs.yjmyzz.model.UserAttempts; 18 19 @Repository 20 public class UserDetailsDaoImpl extends JdbcDaoSupport implements 21 UserDetailsDao { 22 23 private static final String SQL_USERS_UPDATE_LOCKED = "UPDATE t_users SET d_accountnonlocked = ? WHERE d_username = ?"; 24 private static final String SQL_USERS_COUNT = "SELECT COUNT(*) FROM t_users WHERE d_username = ?"; 25 26 private static final String SQL_USER_ATTEMPTS_GET = "SELECT d_id id,d_username username,d_attempts attempts,d_lastmodified lastmodified FROM t_user_attempts WHERE d_username = ?"; 27 private static final String SQL_USER_ATTEMPTS_INSERT = "INSERT INTO t_user_attempts (d_id,d_username, d_attempts, d_lastmodified) VALUES(t_user_attempts_seq.nextval,?,?,?)"; 28 private static final String SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS = "UPDATE t_user_attempts SET d_attempts = d_attempts + 1, d_lastmodified = ? WHERE d_username = ?"; 29 private static final String SQL_USER_ATTEMPTS_RESET_ATTEMPTS = "UPDATE t_user_attempts SET d_attempts = 0, d_lastmodified = null WHERE d_username = ?"; 30 31 private static final int MAX_ATTEMPTS = 3; 32 33 @Autowired 34 private DataSource dataSource; 35 36 @PostConstruct 37 private void initialize() { 38 setDataSource(dataSource); 39 } 40 41 @Override 42 public void updateFailAttempts(String username) { 43 UserAttempts user = getUserAttempts(username); 44 if (user == null) { 45 if (isUserExists(username)) { 46 // if no record, insert a new 47 getJdbcTemplate().update(SQL_USER_ATTEMPTS_INSERT, 48 new Object[] { username, 1, new Date() }); 49 } 50 } else { 51 52 if (isUserExists(username)) { 53 // update attempts count, +1 54 getJdbcTemplate().update(SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS, 55 new Object[] { new Date(), username }); 56 } 57 58 if (user.getAttempts() + 1 >= MAX_ATTEMPTS) { 59 // locked user 60 getJdbcTemplate().update(SQL_USERS_UPDATE_LOCKED, 61 new Object[] { false, username }); 62 // throw exception 63 throw new LockedException("User Account is locked!"); 64 } 65 66 } 67 } 68 69 @Override 70 public void resetFailAttempts(String username) { 71 getJdbcTemplate().update(SQL_USER_ATTEMPTS_RESET_ATTEMPTS, 72 new Object[] { username }); 73 74 } 75 76 @Override 77 public UserAttempts getUserAttempts(String username) { 78 try { 79 80 UserAttempts userAttempts = getJdbcTemplate().queryForObject( 81 SQL_USER_ATTEMPTS_GET, new Object[] { username }, 82 new RowMapper<UserAttempts>() { 83 public UserAttempts mapRow(ResultSet rs, int rowNum) 84 throws SQLException { 85 86 UserAttempts user = new UserAttempts(); 87 user.setId(rs.getInt("id")); 88 user.setUsername(rs.getString("username")); 89 user.setAttempts(rs.getInt("attempts")); 90 user.setLastModified(rs.getDate("lastModified")); 91 92 return user; 93 } 94 95 }); 96 return userAttempts; 97 98 } catch (EmptyResultDataAccessException e) { 99 return null; 100 } 101 102 } 103 104 private boolean isUserExists(String username) { 105 106 boolean result = false; 107 108 int count = getJdbcTemplate().queryForObject(SQL_USERS_COUNT, 109 new Object[] { username }, Integer.class); 110 if (count > 0) { 111 result = true; 112 } 113 114 return result; 115 } 116 117 }
观察代码可以发现,对登录尝试次数的限制处理主要就在上面这个类中,登录尝试次数达到阈值3时,通过抛出异常LockedException来通知上层代码。
三、创建CustomUserDetailsService、LimitLoginAuthenticationProvider
1 package com.cnblogs.yjmyzz.service; 2 3 import java.sql.ResultSet; 4 import java.sql.SQLException; 5 import java.util.List; 6 7 import org.springframework.jdbc.core.RowMapper; 8 import org.springframework.security.core.GrantedAuthority; 9 import org.springframework.security.core.authority.AuthorityUtils; 10 import org.springframework.security.core.userdetails.User; 11 import org.springframework.security.core.userdetails.UserDetails; 12 import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl; 13 import org.springframework.stereotype.Service; 14 15 @Service("userDetailsService") 16 public class CustomUserDetailsService extends JdbcDaoImpl { 17 @Override 18 public void setUsersByUsernameQuery(String usersByUsernameQueryString) { 19 super.setUsersByUsernameQuery(usersByUsernameQueryString); 20 } 21 22 @Override 23 public void setAuthoritiesByUsernameQuery(String queryString) { 24 super.setAuthoritiesByUsernameQuery(queryString); 25 } 26 27 // override to pass get accountNonLocked 28 @Override 29 public List<UserDetails> loadUsersByUsername(String username) { 30 return getJdbcTemplate().query(super.getUsersByUsernameQuery(), 31 new String[] { username }, new RowMapper<UserDetails>() { 32 public UserDetails mapRow(ResultSet rs, int rowNum) 33 throws SQLException { 34 String username = rs.getString("username"); 35 String password = rs.getString("password"); 36 boolean enabled = rs.getBoolean("enabled"); 37 boolean accountNonExpired = rs 38 .getBoolean("accountNonExpired"); 39 boolean credentialsNonExpired = rs 40 .getBoolean("credentialsNonExpired"); 41 boolean accountNonLocked = rs 42 .getBoolean("accountNonLocked"); 43 44 return new User(username, password, enabled, 45 accountNonExpired, credentialsNonExpired, 46 accountNonLocked, AuthorityUtils.NO_AUTHORITIES); 47 } 48 49 }); 50 } 51 52 // override to pass accountNonLocked 53 @Override 54 public UserDetails createUserDetails(String username, 55 UserDetails userFromUserQuery, 56 List<GrantedAuthority> combinedAuthorities) { 57 String returnUsername = userFromUserQuery.getUsername(); 58 59 if (super.isUsernameBasedPrimaryKey()) { 60 returnUsername = username; 61 } 62 63 return new User(returnUsername, userFromUserQuery.getPassword(), 64 userFromUserQuery.isEnabled(), 65 userFromUserQuery.isAccountNonExpired(), 66 userFromUserQuery.isCredentialsNonExpired(), 67 userFromUserQuery.isAccountNonLocked(), combinedAuthorities); 68 } 69 }
为什么需要这个类?因为下面这个类需要它:
1 package com.cnblogs.yjmyzz.provider; 2 3 import java.util.Date; 4 5 import org.springframework.security.authentication.BadCredentialsException; 6 import org.springframework.security.authentication.LockedException; 7 import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 8 import org.springframework.security.core.Authentication; 9 import org.springframework.security.core.AuthenticationException; 10 import org.springframework.stereotype.Component; 11 12 import com.cnblogs.yjmyzz.dao.UserDetailsDao; 13 import com.cnblogs.yjmyzz.model.UserAttempts; 14 15 @Component("authenticationProvider") 16 public class LimitLoginAuthenticationProvider extends DaoAuthenticationProvider { 17 UserDetailsDao userDetailsDao; 18 19 @Override 20 public Authentication authenticate(Authentication authentication) 21 throws AuthenticationException { 22 23 try { 24 25 Authentication auth = super.authenticate(authentication); 26 27 // if reach here, means login success, else exception will be thrown 28 // reset the user_attempts 29 userDetailsDao.resetFailAttempts(authentication.getName()); 30 31 return auth; 32 33 } catch (BadCredentialsException e) { 34 35 userDetailsDao.updateFailAttempts(authentication.getName()); 36 throw e; 37 38 } catch (LockedException e) { 39 40 String error = ""; 41 UserAttempts userAttempts = userDetailsDao 42 .getUserAttempts(authentication.getName()); 43 if (userAttempts != null) { 44 Date lastAttempts = userAttempts.getLastModified(); 45 error = "User account is locked! <br><br>Username : " 46 + authentication.getName() + "<br>Last Attempts : " 47 + lastAttempts; 48 } else { 49 error = e.getMessage(); 50 } 51 52 throw new LockedException(error); 53 } 54 55 } 56 57 public UserDetailsDao getUserDetailsDao() { 58 return userDetailsDao; 59 } 60 61 public void setUserDetailsDao(UserDetailsDao userDetailsDao) { 62 this.userDetailsDao = userDetailsDao; 63 } 64 }
这个类继承自org.springframework.security.authentication.dao.DaoAuthenticationProvider,而DaoAuthenticationProvider里需要一个UserDetailsService的实例,即我们刚才创建的CustomUserDetailService
LimitLoginAuthenticationProvider这个类如何使用呢?该配置文件出场了
四、spring-security.xml
1 <beans:beans xmlns="http://www.springframework.org/schema/security" 2 xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://www.springframework.org/schema/beans 4 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 5 http://www.springframework.org/schema/security 6 http://www.springframework.org/schema/security/spring-security-3.2.xsd"> 7 8 <http auto-config="true" use-expressions="true"> 9 <intercept-url pattern="/admin**" access="hasRole(‘ADMIN‘)" /> 10 <!-- access denied page --> 11 <access-denied-handler error-page="/403" /> 12 <form-login login-page="/login" default-target-url="/welcome" 13 authentication-failure-url="/login?error" username-parameter="username" 14 password-parameter="password" /> 15 <logout logout-success-url="/login?logout" /> 16 <csrf /> 17 </http> 18 19 <beans:bean id="userDetailsDao" 20 class="com.cnblogs.yjmyzz.dao.impl.UserDetailsDaoImpl"> 21 <beans:property name="dataSource" ref="dataSource" /> 22 </beans:bean> 23 24 <beans:bean id="customUserDetailsService" 25 class="com.cnblogs.yjmyzz.service.CustomUserDetailsService"> 26 <beans:property name="usersByUsernameQuery" 27 value="SELECT d_username username,d_password password, d_enabled enabled,d_accountnonexpired accountnonexpired,d_accountnonlocked accountnonlocked,d_credentialsnonexpired credentialsnonexpired FROM t_users WHERE d_username=?" /> 28 <beans:property name="authoritiesByUsernameQuery" 29 value="SELECT d_username username, d_role role FROM t_user_roles WHERE d_username=?" /> 30 <beans:property name="dataSource" ref="dataSource" /> 31 </beans:bean> 32 33 <beans:bean id="authenticationProvider" 34 class="com.cnblogs.yjmyzz.provider.LimitLoginAuthenticationProvider"> 35 <beans:property name="passwordEncoder" ref="encoder" /> 36 <beans:property name="userDetailsService" ref="customUserDetailsService" /> 37 <beans:property name="userDetailsDao" ref="userDetailsDao" /> 38 </beans:bean> 39 40 <beans:bean id="encoder" 41 class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"> 42 <beans:constructor-arg name="strength" value="9" /> 43 </beans:bean> 44 45 46 <authentication-manager> 47 <authentication-provider ref="authenticationProvider" /> 48 </authentication-manager> 49 50 </beans:beans>
跟之前的变化有点大,47行是核心,为了实现47行的注入,需要33-38行,而为了完成authenticationProvider所需的一些property的注入,又需要其它bean的注入,所以看上去增加的内容就有点多了,但并不难理解。
五、运行效果
连续3次输错密码后,将看到下面的提示
这时如果查下数据库,会看到
错误尝试次数,在db中已经达到阀值3
而且该用户的“是否未锁定”字段值为0,如果要手动解锁,把该值恢复为1,并将T_USER_ATTEMPTS中的尝试次数,改到3以下即可。
源代码下载:SpringSecurity-Limit-Login-Attempts-XML.zip
参考文章:
Spring Security : limit login attempts example