Spring Security笔记:登录尝试次数限制_spring-security本文演示了Spring mvc3 + Spring Security3框架中,如何限制用户登录的错误尝试次数



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

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

一、表结构调整

T_USERS增加了如下3个字段:

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

D_ACCOUNTNONEXPIRED,NUMBER(1) -- 表示帐号是否未过期

D_ACCOUNTNONLOCKED,NUMBER(1), -- 表示帐号是否未锁定

D_CREDENTIALSNONEXPIRED,NUMBER(1) --表示登录凭据是否未过期

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

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

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

D_ID 是流水号

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

D_ATTEMPTS 登录次数

D_LASTMODIFIED 最后登录错误的日期

 

二、创建Model/DAO/DAOImpl

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

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

T_USER_ATTEMPTS表对应的Model如下


Spring Security笔记:登录尝试次数限制_spring-security_05Spring Security笔记:登录尝试次数限制_spring-security_06


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笔记:登录尝试次数限制_spring-security_05Spring Security笔记:登录尝试次数限制_spring-security_06


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笔记:登录尝试次数限制_spring-security_05Spring Security笔记:登录尝试次数限制_spring-security_06


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笔记:登录尝试次数限制_spring-security_05Spring Security笔记:登录尝试次数限制_spring-security_06


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笔记:登录尝试次数限制_spring-security_05Spring Security笔记:登录尝试次数限制_spring-security_06


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笔记:登录尝试次数限制_spring-mvc_15

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

 

四、spring-security.xml


Spring Security笔记:登录尝试次数限制_spring-security_05Spring Security笔记:登录尝试次数限制_spring-security_06


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笔记:登录尝试次数限制_attempts_18

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

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

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

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

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

 

作者:菩提树下的杨过