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如下

package com.cnblogs.yjmyzz.model;

import java.util.Date;

public class UserAttempts {

    private int id;

    private String username;
    private int attempts;
    private Date lastModified;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAttempts() {
        return attempts;
    }

    public void setAttempts(int attempts) {
        this.attempts = attempts;
    }

    public Date getLastModified() {
        return lastModified;
    }

    public void setLastModified(Date lastModified) {
        this.lastModified = lastModified;
    }

}

UserAttempts

 对应的DAO接口

package com.cnblogs.yjmyzz.dao;

import com.cnblogs.yjmyzz.model.UserAttempts;

public interface UserDetailsDao {

    void updateFailAttempts(String username);

    void resetFailAttempts(String username);

    UserAttempts getUserAttempts(String username);

}

UserDetailsDao

 以及DAO接口的实现

package com.cnblogs.yjmyzz.dao.impl;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.stereotype.Repository;
import org.springframework.security.authentication.LockedException;
import com.cnblogs.yjmyzz.dao.UserDetailsDao;
import com.cnblogs.yjmyzz.model.UserAttempts;

@Repository
public class UserDetailsDaoImpl extends JdbcDaoSupport implements
        UserDetailsDao {

    private static final String SQL_USERS_UPDATE_LOCKED = "UPDATE t_users SET d_accountnonlocked = ? WHERE d_username = ?";
    private static final String SQL_USERS_COUNT = "SELECT COUNT(*) FROM t_users WHERE d_username = ?";

    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 = ?";
    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,?,?,?)";
    private static final String SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS = "UPDATE t_user_attempts SET d_attempts = d_attempts + 1, d_lastmodified = ? WHERE d_username = ?";
    private static final String SQL_USER_ATTEMPTS_RESET_ATTEMPTS = "UPDATE t_user_attempts SET d_attempts = 0, d_lastmodified = null WHERE d_username = ?";

    private static final int MAX_ATTEMPTS = 3;

    @Autowired
    private DataSource dataSource;

    @PostConstruct
    private void initialize() {
        setDataSource(dataSource);
    }

    @Override
    public void updateFailAttempts(String username) {
        UserAttempts user = getUserAttempts(username);
        if (user == null) {
            if (isUserExists(username)) {
                // if no record, insert a new
                getJdbcTemplate().update(SQL_USER_ATTEMPTS_INSERT,
                        new Object[] { username, 1, new Date() });
            }
        } else {

            if (isUserExists(username)) {
                // update attempts count, +1
                getJdbcTemplate().update(SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS,
                        new Object[] { new Date(), username });
            }

            if (user.getAttempts() + 1 >= MAX_ATTEMPTS) {
                // locked user
                getJdbcTemplate().update(SQL_USERS_UPDATE_LOCKED,
                        new Object[] { false, username });
                // throw exception
                throw new LockedException("User Account is locked!");
            }

        }
    }

    @Override
    public void resetFailAttempts(String username) {
        getJdbcTemplate().update(SQL_USER_ATTEMPTS_RESET_ATTEMPTS,
                new Object[] { username });

    }

    @Override
    public UserAttempts getUserAttempts(String username) {
        try {

            UserAttempts userAttempts = getJdbcTemplate().queryForObject(
                    SQL_USER_ATTEMPTS_GET, new Object[] { username },
                    new RowMapper<UserAttempts>() {
                        public UserAttempts mapRow(ResultSet rs, int rowNum)
                                throws SQLException {

                            UserAttempts user = new UserAttempts();
                            user.setId(rs.getInt("id"));
                            user.setUsername(rs.getString("username"));
                            user.setAttempts(rs.getInt("attempts"));
                            user.setLastModified(rs.getDate("lastModified"));

                            return user;
                        }

                    });
            return userAttempts;

        } catch (EmptyResultDataAccessException e) {
            return null;
        }

    }

    private boolean isUserExists(String username) {

        boolean result = false;

        int count = getJdbcTemplate().queryForObject(SQL_USERS_COUNT,
                new Object[] { username }, Integer.class);
        if (count > 0) {
            result = true;
        }

        return result;
    }

}

UserDetailsDaoImpl

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

三、创建CustomUserDetailsService、LimitLoginAuthenticationProvider

package com.cnblogs.yjmyzz.service;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.stereotype.Service;

@Service("userDetailsService")
public class CustomUserDetailsService extends JdbcDaoImpl {
    @Override
    public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
        super.setUsersByUsernameQuery(usersByUsernameQueryString);
    }

    @Override
    public void setAuthoritiesByUsernameQuery(String queryString) {
        super.setAuthoritiesByUsernameQuery(queryString);
    }

    // override to pass get accountNonLocked
    @Override
    public List<UserDetails> loadUsersByUsername(String username) {
        return getJdbcTemplate().query(super.getUsersByUsernameQuery(),
                new String[] { username }, new RowMapper<UserDetails>() {
                    public UserDetails mapRow(ResultSet rs, int rowNum)
                            throws SQLException {
                        String username = rs.getString("username");
                        String password = rs.getString("password");
                        boolean enabled = rs.getBoolean("enabled");
                        boolean accountNonExpired = rs
                                .getBoolean("accountNonExpired");
                        boolean credentialsNonExpired = rs
                                .getBoolean("credentialsNonExpired");
                        boolean accountNonLocked = rs
                                .getBoolean("accountNonLocked");

                        return new User(username, password, enabled,
                                accountNonExpired, credentialsNonExpired,
                                accountNonLocked, AuthorityUtils.NO_AUTHORITIES);
                    }

                });
    }

    // override to pass accountNonLocked
    @Override
    public UserDetails createUserDetails(String username,
            UserDetails userFromUserQuery,
            List<GrantedAuthority> combinedAuthorities) {
        String returnUsername = userFromUserQuery.getUsername();

        if (super.isUsernameBasedPrimaryKey()) {
            returnUsername = username;
        }

        return new User(returnUsername, userFromUserQuery.getPassword(),
                userFromUserQuery.isEnabled(),
                userFromUserQuery.isAccountNonExpired(),
                userFromUserQuery.isCredentialsNonExpired(),
                userFromUserQuery.isAccountNonLocked(), combinedAuthorities);
    }
}

CustomUserDetailsService

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

package com.cnblogs.yjmyzz.provider;

import java.util.Date;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import com.cnblogs.yjmyzz.dao.UserDetailsDao;
import com.cnblogs.yjmyzz.model.UserAttempts;

@Component("authenticationProvider")
public class LimitLoginAuthenticationProvider extends DaoAuthenticationProvider {
    UserDetailsDao userDetailsDao;

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        try {

            Authentication auth = super.authenticate(authentication);

            // if reach here, means login success, else exception will be thrown
            // reset the user_attempts
            userDetailsDao.resetFailAttempts(authentication.getName());

            return auth;

        } catch (BadCredentialsException e) {

            userDetailsDao.updateFailAttempts(authentication.getName());
            throw e;

        } catch (LockedException e) {

            String error = "";
            UserAttempts userAttempts = userDetailsDao
                    .getUserAttempts(authentication.getName());
            if (userAttempts != null) {
                Date lastAttempts = userAttempts.getLastModified();
                error = "User account is locked! <br><br>Username : "
                        + authentication.getName() + "<br>Last Attempts : "
                        + lastAttempts;
            } else {
                error = e.getMessage();
            }

            throw new LockedException(error);
        }

    }

    public UserDetailsDao getUserDetailsDao() {
        return userDetailsDao;
    }

    public void setUserDetailsDao(UserDetailsDao userDetailsDao) {
        this.userDetailsDao = userDetailsDao;
    }
}

LimitLoginAuthenticationProvider

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

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

四、spring-security.xml

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.2.xsd">

    <http auto-config="true" use-expressions="true">
        <intercept-url pattern="/admin**" access="hasRole('ADMIN')" />
        <!-- access denied page -->
        <access-denied-handler error-page="/403" />
        <form-login login-page="/login" default-target-url="/welcome"
            authentication-failure-url="/login?error" username-parameter="username"
            password-parameter="password" />
        <logout logout-success-url="/login?logout" />
        <csrf />
    </http>

    <beans:bean id="userDetailsDao"
        class="com.cnblogs.yjmyzz.dao.impl.UserDetailsDaoImpl">
        <beans:property name="dataSource" ref="dataSource" />
    </beans:bean>

    <beans:bean id="customUserDetailsService"
        class="com.cnblogs.yjmyzz.service.CustomUserDetailsService">
        <beans:property name="usersByUsernameQuery"
            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=?" />
        <beans:property name="authoritiesByUsernameQuery"
            value="SELECT d_username username, d_role role FROM t_user_roles WHERE d_username=?" />
        <beans:property name="dataSource" ref="dataSource" />
    </beans:bean>

    <beans:bean id="authenticationProvider"
        class="com.cnblogs.yjmyzz.provider.LimitLoginAuthenticationProvider">
        <beans:property name="passwordEncoder" ref="encoder" />
        <beans:property name="userDetailsService" ref="customUserDetailsService" />
        <beans:property name="userDetailsDao" ref="userDetailsDao" />
    </beans:bean>

    <beans:bean id="encoder"
        class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
        <beans:constructor-arg name="strength" value="9" />
    </beans:bean>


    <authentication-manager>
        <authentication-provider ref="authenticationProvider" />
    </authentication-manager>

</beans:beans>

 

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

猜你喜欢

转载自zzc1684.iteye.com/blog/2112414