zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    Java学习心得16
    Java学习心得15
    Java学习心得14
    Java学习心得13
    Java学习心得12
    Java学习心得11
    Java学习心得10
    Java学习心得09
    Java学习心得08
    JVM垃圾回收
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/limit-login-attempts-in-spring-security3.html
Copyright © 2011-2022 走看看