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

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

一、表结构调整

T_USERS增加了如下3个字段:

java session重复登录 java登录次数限制_ide

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

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

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

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

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

java session重复登录 java登录次数限制_ide_02

D_ID 是流水号

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

D_ATTEMPTS 登录次数

D_LASTMODIFIED 最后登录错误的日期

二、创建Model/DAO/DAOImpl

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

java session重复登录 java登录次数限制_bc_03

T_USER_ATTEMPTS表对应的Model如下

java session重复登录 java登录次数限制_bc_04

java session重复登录 java登录次数限制_ide_05

1 packagecom.cnblogs.yjmyzz.model;2
3 importjava.util.Date;4
5 public classUserAttempts {6
7 private intid;8
9 privateString username;10 private intattempts;11 privateDate lastModified;12
13 public intgetId() {14 returnid;15 }16
17 public void setId(intid) {18 this.id =id;19 }20
21 publicString getUsername() {22 returnusername;23 }24
25 public voidsetUsername(String username) {26 this.username =username;27 }28
29 public intgetAttempts() {30 returnattempts;31 }32
33 public void setAttempts(intattempts) {34 this.attempts =attempts;35 }36
37 publicDate getLastModified() {38 returnlastModified;39 }40
41 public voidsetLastModified(Date lastModified) {42 this.lastModified =lastModified;43 }44
45 }
UserAttempts

对应的DAO接口

java session重复登录 java登录次数限制_bc_04

java session重复登录 java登录次数限制_ide_05

1 packagecom.cnblogs.yjmyzz.dao;2
3 importcom.cnblogs.yjmyzz.model.UserAttempts;4
5 public interfaceUserDetailsDao {6
7 voidupdateFailAttempts(String username);8
9 voidresetFailAttempts(String username);10
11 UserAttempts getUserAttempts(String username);12
13 }
UserDetailsDao

以及DAO接口的实现

java session重复登录 java登录次数限制_bc_04

java session重复登录 java登录次数限制_ide_05

1 packagecom.cnblogs.yjmyzz.dao.impl;2
3 importjava.sql.ResultSet;4 importjava.sql.SQLException;5 importjava.util.Date;6
7 importjavax.annotation.PostConstruct;8 importjavax.sql.DataSource;9
10 importorg.springframework.beans.factory.annotation.Autowired;11 importorg.springframework.dao.EmptyResultDataAccessException;12 importorg.springframework.jdbc.core.RowMapper;13 importorg.springframework.jdbc.core.support.JdbcDaoSupport;14 importorg.springframework.stereotype.Repository;15 importorg.springframework.security.authentication.LockedException;16 importcom.cnblogs.yjmyzz.dao.UserDetailsDao;17 importcom.cnblogs.yjmyzz.model.UserAttempts;18
19 @Repository20 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 @Autowired34 privateDataSource dataSource;35
36 @PostConstruct37 private voidinitialize() {38 setDataSource(dataSource);39 }40
41 @Override42 public voidupdateFailAttempts(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, newDate() });49 }50 } else{51
52 if(isUserExists(username)) {53 //update attempts count, +1
54 getJdbcTemplate().update(SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS,55 new Object[] { newDate(), 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 @Override70 public voidresetFailAttempts(String username) {71 getJdbcTemplate().update(SQL_USER_ATTEMPTS_RESET_ATTEMPTS,72 newObject[] { username });73
74 }75
76 @Override77 publicUserAttempts getUserAttempts(String username) {78 try{79
80 UserAttempts userAttempts =getJdbcTemplate().queryForObject(81 SQL_USER_ATTEMPTS_GET, newObject[] { username },82 new RowMapper() {83 public UserAttempts mapRow(ResultSet rs, introwNum)84 throwsSQLException {85
86 UserAttempts user = newUserAttempts();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 returnuser;93 }94
95 });96 returnuserAttempts;97
98 } catch(EmptyResultDataAccessException e) {99 return null;100 }101
102 }103
104 private booleanisUserExists(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 returnresult;115 }116
117 }
UserDetailsDaoImpl

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

三、创建CustomUserDetailsService、LimitLoginAuthenticationProvider

java session重复登录 java登录次数限制_bc_04

java session重复登录 java登录次数限制_ide_05

1 packagecom.cnblogs.yjmyzz.service;2
3 importjava.sql.ResultSet;4 importjava.sql.SQLException;5 importjava.util.List;6
7 importorg.springframework.jdbc.core.RowMapper;8 importorg.springframework.security.core.GrantedAuthority;9 importorg.springframework.security.core.authority.AuthorityUtils;10 importorg.springframework.security.core.userdetails.User;11 importorg.springframework.security.core.userdetails.UserDetails;12 importorg.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;13 importorg.springframework.stereotype.Service;14
15 @Service("userDetailsService")16 public class CustomUserDetailsService extendsJdbcDaoImpl {17 @Override18 public voidsetUsersByUsernameQuery(String usersByUsernameQueryString) {19 super.setUsersByUsernameQuery(usersByUsernameQueryString);20 }21
22 @Override23 public voidsetAuthoritiesByUsernameQuery(String queryString) {24 super.setAuthoritiesByUsernameQuery(queryString);25 }26
27 //override to pass get accountNonLocked
28 @Override29 public ListloadUsersByUsername(String username) {30 return getJdbcTemplate().query(super.getUsersByUsernameQuery(),31 new String[] { username }, new RowMapper() {32 public UserDetails mapRow(ResultSet rs, introwNum)33 throwsSQLException {34 String username = rs.getString("username");35 String password = rs.getString("password");36 boolean enabled = rs.getBoolean("enabled");37 boolean accountNonExpired =rs38 .getBoolean("accountNonExpired");39 boolean credentialsNonExpired =rs40 .getBoolean("credentialsNonExpired");41 boolean accountNonLocked =rs42 .getBoolean("accountNonLocked");43
44 return newUser(username, password, enabled,45 accountNonExpired, credentialsNonExpired,46 accountNonLocked, AuthorityUtils.NO_AUTHORITIES);47 }48
49 });50 }51
52 //override to pass accountNonLocked
53 @Override54 publicUserDetails createUserDetails(String username,55 UserDetails userFromUserQuery,56 ListcombinedAuthorities) {57 String returnUsername =userFromUserQuery.getUsername();58
59 if (super.isUsernameBasedPrimaryKey()) {60 returnUsername =username;61 }62
63 return newUser(returnUsername, userFromUserQuery.getPassword(),64 userFromUserQuery.isEnabled(),65 userFromUserQuery.isAccountNonExpired(),66 userFromUserQuery.isCredentialsNonExpired(),67 userFromUserQuery.isAccountNonLocked(), combinedAuthorities);68 }69 }
CustomUserDetailsService

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

java session重复登录 java登录次数限制_bc_04

java session重复登录 java登录次数限制_ide_05

1 packagecom.cnblogs.yjmyzz.provider;2
3 importjava.util.Date;4
5 importorg.springframework.security.authentication.BadCredentialsException;6 importorg.springframework.security.authentication.LockedException;7 importorg.springframework.security.authentication.dao.DaoAuthenticationProvider;8 importorg.springframework.security.core.Authentication;9 importorg.springframework.security.core.AuthenticationException;10 importorg.springframework.stereotype.Component;11
12 importcom.cnblogs.yjmyzz.dao.UserDetailsDao;13 importcom.cnblogs.yjmyzz.model.UserAttempts;14
15 @Component("authenticationProvider")16 public class LimitLoginAuthenticationProvider extendsDaoAuthenticationProvider {17 UserDetailsDao userDetailsDao;18
19 @Override20 publicAuthentication authenticate(Authentication authentication)21 throwsAuthenticationException {22
23 try{24
25 Authentication auth = super.authenticate(authentication);26
27 //if reach here, means login success, else exception will be thrown28 //reset the user_attempts
29 userDetailsDao.resetFailAttempts(authentication.getName());30
31 returnauth;32
33 } catch(BadCredentialsException e) {34
35 userDetailsDao.updateFailAttempts(authentication.getName());36 throwe;37
38 } catch(LockedException e) {39
40 String error = "";41 UserAttempts userAttempts =userDetailsDao42 .getUserAttempts(authentication.getName());43 if (userAttempts != null) {44 Date lastAttempts =userAttempts.getLastModified();45 error = "User account is locked! 
Username : "46 + authentication.getName() + "
Last Attempts : "47 +lastAttempts;48 } else{49 error =e.getMessage();50 }51
52 throw newLockedException(error);53 }54
55 }56
57 publicUserDetailsDao getUserDetailsDao() {58 returnuserDetailsDao;59 }60
61 public voidsetUserDetailsDao(UserDetailsDao userDetailsDao) {62 this.userDetailsDao =userDetailsDao;63 }64 }
LimitLoginAuthenticationProvider

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

java session重复登录 java登录次数限制_bc_14

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

四、spring-security.xml

java session重复登录 java登录次数限制_bc_04

java session重复登录 java登录次数限制_ide_05

1 
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/beans4 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd5 http://www.springframework.org/schema/security6 http://www.springframework.org/schema/security/spring-security-3.2.xsd">
7
8 
9 
10 
11 
12 
13 authentication-failure-url="/login?error"username-parameter="username"
14 password-parameter="password" />
15 
16 
17 
18
19 
20 class="com.cnblogs.yjmyzz.dao.impl.UserDetailsDaoImpl">
21 
22 
23
24 
25 class="com.cnblogs.yjmyzz.service.CustomUserDetailsService">
26 
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 
29 value="SELECT d_username username, d_role role FROM t_user_roles WHERE d_username=?" />
30 
31 
32
33 
34 class="com.cnblogs.yjmyzz.provider.LimitLoginAuthenticationProvider">
35 
36 
37 
38 
39
40 
41 class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
42 
43 
44
45
46 
47 
48 
49
50 
View Code

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

五、运行效果

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

java session重复登录 java登录次数限制_ide_17

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

java session重复登录 java登录次数限制_ide_18

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

java session重复登录 java登录次数限制_java 登录次数限制_19

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