Spring Security笔记:登录尝试次数限制

今天在前面一节的基础之上,再增加一点新内容,默认情况下Spring Security不会对登录错误的尝试次数做限制,也就是说允许暴力尝试,这显然不够安全,下面的内容将带着大家一起学习如何限制登录尝试次数。

首先对之前创建的数据库表做点小调整

一、表结构调整

T_USERS增加了如下3个字段:

Spring Security笔记:登录尝试次数限制

D_ACCOUNTNONEXPIRED,NUMBER(1) -- 表示帐号是否未过期
D_ACCOUNTNONLOCKED,NUMBER(1), -- 表示帐号是否未锁定
D_CREDENTIALSNONEXPIRED,NUMBER(1) --表示登录凭据是否未过期

要实现登录次数的限制,其实起作用的字段是D_ACCOUNTNONLOCKED,值为1时,表示正常,为0时表示被锁定,另外二个字段的作用以后的学习内容会详细解释。

新增一张表T_USER_ATTEMPTS,用来辅助记录每个用户登录错误时的尝试次数

Spring Security笔记:登录尝试次数限制

D_ID 是流水号

D_USERNAME 用户名,外建引用T_USERS中的D_USERNAME

D_ATTEMPTS 登录次数

D_LASTMODIFIED 最后登录错误的日期

 

二、创建Model/DAO/DAOImpl

要对新加的T_USER_ATTEMPTS读写数据,得有一些操作DB的类,这里我们采用Spring的JDBCTemplate来处理,包结构参考下图:

Spring Security笔记:登录尝试次数限制

T_USER_ATTEMPTS表对应的Model如下

Spring Security笔记:登录尝试次数限制
 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 }
UserAttempts

对应的DAO接口

Spring Security笔记:登录尝试次数限制
 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 }
UserDetailsDao

以及DAO接口的实现

Spring Security笔记:登录尝试次数限制
  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 }
UserDetailsDaoImpl

观察代码可以发现,对登录尝试次数的限制处理主要就在上面这个类中,登录尝试次数达到阈值3时,通过抛出异常LockedException来通知上层代码。

 

三、创建CustomUserDetailsService、LimitLoginAuthenticationProvider

Spring Security笔记:登录尝试次数限制
 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 }
CustomUserDetailsService

为什么需要这个类?因为下面这个类需要它:

Spring Security笔记:登录尝试次数限制
 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 }
LimitLoginAuthenticationProvider

这个类继承自org.springframework.security.authentication.dao.DaoAuthenticationProvider,而DaoAuthenticationProvider里需要一个UserDetailsService的实例,即我们刚才创建的CustomUserDetailService

Spring Security笔记:登录尝试次数限制

LimitLoginAuthenticationProvider这个类如何使用呢?该配置文件出场了

 

四、spring-security.xml

Spring Security笔记:登录尝试次数限制
 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>
View Code

跟之前的变化有点大,47行是核心,为了实现47行的注入,需要33-38行,而为了完成authenticationProvider所需的一些property的注入,又需要其它bean的注入,所以看上去增加的内容就有点多了,但并不难理解。

 

五、运行效果

连续3次输错密码后,将看到下面的提示

Spring Security笔记:登录尝试次数限制

这时如果查下数据库,会看到

Spring Security笔记:登录尝试次数限制

错误尝试次数,在db中已经达到阀值3

Spring Security笔记:登录尝试次数限制

而且该用户的“是否未锁定”字段值为0,如果要手动解锁,把该值恢复为1,并将T_USER_ATTEMPTS中的尝试次数,改到3以下即可。

 

源代码下载:SpringSecurity-Limit-Login-Attempts-XML.zip
参考文章: Spring Security : limit login attempts example

Spring Security笔记:登录尝试次数限制,布布扣,bubuko.com

Spring Security笔记:登录尝试次数限制

上一篇:Cocos2d-x Win32中使用Visual Leak Detector (for VC++)检查内存泄漏


下一篇:Search for a Range leetcode java