开发者社区> 杨俊明> 正文

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

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

今天在前面一节的基础之上,再增加一点新内容,默认情况下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 }
UserAttempts

对应的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 }
UserDetailsDao

以及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 }
UserDetailsDaoImpl

观察代码可以发现,对登录尝试次数的限制处理主要就在上面这个类中,登录尝试次数达到阈值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 }
CustomUserDetailsService

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

 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

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>
View Code

跟之前的变化有点大,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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Spring Security笔记:自定义登录页
以下内容参考了 http://www.mkyong.com/spring-security/spring-security-form-login-example/ 接上回,在前面的Hello World示例中,Spring Security为我们自动生成了默认登录页,对于大多数项目而言,如此简单的...
986 0
Spring-Aop处理记录日志
AOP的几种通知类型: 1,前置通知(Before advice):在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行 配置文件中使用 &lt;aop:before&gt;进行声明 注解使用 @Before 进行声明 2,后置通知(After advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
2527 0
Spring Security笔记:使用BCrypt算法加密存储登录密码
在前一节使用数据库进行用户认证(form login using database)里,我们学习了如何把“登录帐号、密码”存储在db中,但是密码都是明文存储的,显然不太讲究。这一节将学习如何使用spring security3新加入的bcrypt算法,将登录加密存储到db中,并正常通过验证。
1783 0
Windows 2008 远程登陆时提示"要登录到此远程计算机,您必须被授予允许通过终端登录登录的权限"
ECS Win2008 远程时提示"要登录到此远程计算机,您必须被授予允许通过终端登录登录的权限"的解决方法   问题描述 ECS Windows 2008 远程登陆时提示"要登录到此远程计算机,您必须被授予允许通过终端登录登录的权限",如下图所示: 问题分析 组策略中做了设置不允许管理员组成员登录。
1290 0
Spring Security笔记:Remember Me(下次自动登录)
前一节学习了如何限制登录尝试次数,今天在这个基础上再增加一点新功能:Remember Me. 很多网站,比如博客园,在登录页面就有这个选项,勾选“下次自动登录”后,在一定时间段内,只要不清空浏览器Cookie,就可以自动登录。
1332 0
SpringBoot-18-之AOP+log4j 记录访问请求信息
org.springframework.boot spring-boot-starter-aop log4j log4j 1.
1123 0
ssh限制登录IP修改端口
在/etc/hosts.allow输入    (其中192.168.10.88是你要允许登陆ssh的ip,或者是一个网段192.168.10.0/24)    sshd:192.168.10.88:allow       在/etc/hosts.deny输入(表示除了上面允许的,其他的ip   都拒绝登陆ssh)    sshd:ALLvi /etc/ssh/sshd_config   比如说你只允许1.1.1.1这个IP进入,其它都禁止: 方法1、iptables。
564 0
工欲善其事必先利其器SecureCRT+VMware® Workstation_学习笔记
时间:2017.12.31作者:李强参考:man,info,magedu讲义,万能的internet实验环境:VMware® Workstation 12 Pro ,Centos 6.9,Centos 7.4,SecureCRT Version 8.1.4声明:以下英文纯属个人翻译,英文B级,欢迎纠正,以下内容纯属个人理解,并没有对错,只是参考,盗版不纠,才能有限,希望不误人子弟为好。
777 0
SpringBoot 整合 Security(二)实现验证码登录
在上一节的基础上,我们再给项目加入验证码模块,security并没有现成的给我们实现这部分功能,所以我们就需要手写过滤器来实现它。这节题目看上去和第一节没什么关系,但是思想大同小异,希望可以耐心的看,毕竟我尽力的去往清楚的写。
1240 0
+关注
杨俊明
菩提树下的杨过 http://yjmyzz.cnblogs.com/
1105
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
OceanBase 入门到实战教程
立即下载
阿里云图数据库GDB,加速开启“图智”未来.ppt
立即下载
实时数仓Hologres技术实战一本通2.0版(下)
立即下载