spring security实现限制登录次数功能

本节是在基于注解方式进行的,后面的例子都会基于注解形式,不再实现XML配置形式,毕竟注解才是趋势嘛!

关键在于实现自定义的UserDetailsService和AuthenticationProvider

项目结构如下:

查看spring security的源代码可以发现默认security已经定义的user中的一些变量,鉴于此创建users表如下:

CREATE TABLE users (
  username VARCHAR(45) NOT NULL,
  password VARCHAR(45) NOT NULL,
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  accountNonExpired BOOLEAN NOT NULL DEFAULT TRUE,
  accountNonLocked BOOLEAN NOT NULL DEFAULT TRUE,
  credentialsNonExpired BOOLEAN NOT NULL DEFAULT TRUE,
  PRIMARY KEY (username)
);

用户角色表user_roles:

CREATE TABLE user_roles (
  user_role_id int(11) NOT NULL AUTO_INCREMENT,
  username varchar(45) NOT NULL,
  role varchar(45) NOT NULL,
  PRIMARY KEY (user_role_id),
  UNIQUE KEY uni_username_role (role,username),
  KEY fk_username_idx (username),
  CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username)
);

用户尝试登陆次数表user_attempts:

CREATE TABLE user_attempts (
  id int(11) NOT NULL AUTO_INCREMENT,
  username varchar(45) NOT NULL,
  attempts varchar(45) NOT NULL,
  lastModified datetime,
  PRIMARY KEY (id)
);

插入数据:

INSERT INTO users(username,password,enabled) VALUES (‘hxf‘,‘123456‘, true);
INSERT INTO users(username,password,enabled) VALUES (‘wpp‘,‘123456‘, true);
INSERT INTO user_roles (username, role) VALUES (‘hxf‘, ‘ROLE_USER‘);
INSERT INTO user_roles (username, role) VALUES (‘hxf‘, ‘ROLE_ADMIN‘);
INSERT INTO user_roles (username, role) VALUES (‘wpp‘, ‘ROLE_USER‘);

一、用户尝试次数类以及相关的操作类

对应user_attempts 表的UserAttempts

package com.petter.model;
import java.util.Date;
/**
 * @author hongxf
 * @since 2017-03-20 10:50
 */
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;
    }
}

对应的操作类,接口UserDetailsDao:

package com.petter.dao;
import com.petter.model.UserAttempts;
/**
 * @author hongxf
 * @since 2017-03-20 10:53
 */
public interface UserDetailsDao {
    void updateFailAttempts(String username);
    void resetFailAttempts(String username);
    UserAttempts getUserAttempts(String username);
}

其实现类UserDetailsDaoImpl 如下,具体见注释:

package com.petter.dao.impl;
import com.petter.dao.UserDetailsDao;
import com.petter.model.UserAttempts;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.security.authentication.LockedException;
import org.springframework.stereotype.Repository;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Date;
/**
 * @author hongxf
 * @since 2017-03-20 10:54
 */
@Repository
public class UserDetailsDaoImpl extends JdbcDaoSupport implements UserDetailsDao {
    private static final String SQL_USERS_UPDATE_LOCKED = "UPDATE USERS SET accountNonLocked = ? WHERE username = ?";
    private static final String SQL_USERS_COUNT = "SELECT count(*) FROM USERS WHERE username = ?";
    private static final String SQL_USER_ATTEMPTS_GET = "SELECT * FROM USER_ATTEMPTS WHERE username = ?";
    private static final String SQL_USER_ATTEMPTS_INSERT = "INSERT INTO USER_ATTEMPTS (USERNAME, ATTEMPTS, LASTMODIFIED) VALUES(?,?,?)";
    private static final String SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS = "UPDATE USER_ATTEMPTS SET attempts = attempts + 1, lastmodified = ? WHERE username = ?";
    private static final String SQL_USER_ATTEMPTS_RESET_ATTEMPTS = "UPDATE USER_ATTEMPTS SET attempts = 0, lastmodified = null WHERE username = ?";
    private static final int MAX_ATTEMPTS = 3;
    @Resource
    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)) {
                // 如果之前没有记录,添加一条
                getJdbcTemplate().update(SQL_USER_ATTEMPTS_INSERT, username, 1, new Date());
            }
        } else {
            if (isUserExists(username)) {
                // 存在用户则失败一次增加一次尝试次数
                getJdbcTemplate().update(SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS, new Date(), username);
            }
            if (user.getAttempts() + 1 >= MAX_ATTEMPTS) {
                // 大于尝试次数则锁定
                getJdbcTemplate().update(SQL_USERS_UPDATE_LOCKED, false, username);
                // 并且抛出账号锁定异常
                throw new LockedException("用户账号已被锁定,请联系管理员解锁");
            }
        }
    }
    @Override
    public UserAttempts getUserAttempts(String username) {
        try {
            UserAttempts userAttempts = getJdbcTemplate().queryForObject(SQL_USER_ATTEMPTS_GET,
                    new Object[] { username }, (rs, rowNum) -> {
                        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;
        }
    }
    @Override
    public void resetFailAttempts(String username) {
        getJdbcTemplate().update(
                SQL_USER_ATTEMPTS_RESET_ATTEMPTS, username);
    }
    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;
    }
}

二、实现自定义的UserDetailsService

由于使用的jdbc方式查询数据库,spring以及帮我们实现了一个UserDetailsService,就是JdbcDaoImpl,查看源代码

package org.springframework.security.core.userdetails.jdbc;
public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService {
  //...
  protected List<UserDetails> loadUsersByUsername(String username) {
    return getJdbcTemplate().query(usersByUsernameQuery, new String[] {username}, new RowMapper<UserDetails>() {
      public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
        String username = rs.getString(1);
        String password = rs.getString(2);
        boolean enabled = rs.getBoolean(3);
        return new User(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);
      }
  });
}

可见已经实现了UserDetailsService,但是它默认设置accountNonLocked总是true,我们在此基础上进行实现 CustomUserDetailsService

package com.petter.service;
import org.springframework.beans.factory.annotation.Value;
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;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;
/**
 * 查看JdbcDaoImpl的源码可以发现这是实现自定义的UserDetailsService
 * 添加上锁定和过期信息
 * @author hongxf
 * @since 2017-03-20 12:30
 */
@Service("userDetailsService")
public class CustomUserDetailsService extends JdbcDaoImpl {
    @Resource
    private DataSource dataSource;
    @PostConstruct
    private void initialize() {
        setDataSource(dataSource);
    }
    @Override
    @Value("select * from users where username = ?")
    public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
        super.setUsersByUsernameQuery(usersByUsernameQueryString);
    }
    @Override
    @Value("select username, role from user_roles where username = ?")
    public void setAuthoritiesByUsernameQuery(String queryString) {
        super.setAuthoritiesByUsernameQuery(queryString);
    }
    @Override
    protected List<UserDetails> loadUsersByUsername(String username) {
        return getJdbcTemplate().query(super.getUsersByUsernameQuery(), new Object[]{username},
                (rs, rowNum) -> {
                    String username1 = 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(username1, password, enabled, accountNonExpired, credentialsNonExpired,
                            accountNonLocked, AuthorityUtils.NO_AUTHORITIES);
                });
    }
    @Override
    protected 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);
    }
}

三、实现自定义的AuthenticationProvider,当每次登录失败以后更新用户尝试次数表

我们仍然可以继承一个类DaoAuthenticationProvider来快速实现

package com.petter.handler;
import com.petter.dao.UserDetailsDao;
import com.petter.model.UserAttempts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
/**
 * 自定义验证程序
 * @author hongxf
 * @since 2017-03-20 14:28
 */
@Component
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {
    @Resource
    private UserDetailsDao userDetailsDao;
    @Autowired
    @Qualifier("userDetailsService")
    @Override
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        super.setUserDetailsService(userDetailsService);
    }
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        try {
            //调用上层验证逻辑
            Authentication auth = super.authenticate(authentication);
            //如果验证通过登录成功则重置尝试次数, 否则抛出异常
            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 = "用户已经被锁定,用户名 : "
                        + authentication.getName() + "最后尝试登陆时间 : " + lastAttempts;
            }else{
                error = e.getMessage();
            }
            throw new LockedException(error);
        }
    }
}

四、根据抛出的异常实现自定义错误信息

修改登录的方法,我们获取session存储的SPRING_SECURITY_LAST_EXCEPTION的值,自定义错误信息

//获取session存储的SPRING_SECURITY_LAST_EXCEPTION的值,自定义错误信息
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView login(
            @RequestParam(value = "error", required = false) String error,
            @RequestParam(value = "logout", required = false) String logout,
            HttpServletRequest request) {
        ModelAndView model = new ModelAndView();
        if (error != null) {
            model.addObject("error", getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION"));
        }
        if (logout != null) {
            model.addObject("msg", "你已经成功退出");
        }
        model.setViewName("login");
        return model;
    }
    //自定义错误类型
    private String getErrorMessage(HttpServletRequest request, String key){
        Exception exception =
                (Exception) request.getSession().getAttribute(key);
        String error;
        if (exception instanceof BadCredentialsException) {
            error = "不正确的用户名或密码";
        }else if(exception instanceof LockedException) {
            error = exception.getMessage();
        }else{
            error = "不正确的用户名或密码";
        }
        return error;
    }

五、最后配置自定义的验证类CustomAuthenticationProvider

修改SecurityConfig

package com.petter.config;
import com.petter.handler.CustomAuthenticationProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import javax.annotation.Resource;
/**
 * 相当于spring-security.xml中的配置
 * @author hongxf
 * @since 2017-03-08 9:30
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private CustomAuthenticationProvider authenticationProvider;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }
    /**
     * 配置权限要求
     * 采用注解方式,默认开启csrf
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/dba/**").hasAnyRole("ADMIN", "DBA")
            .and()
                .formLogin().loginPage("/login")
                .defaultSuccessUrl("/welcome").failureUrl("/login?error")
                .usernameParameter("user-name").passwordParameter("pwd")
            .and()
                .logout().logoutSuccessUrl("/login?logout")
            .and()
                .exceptionHandling().accessDeniedPage("/403")
            .and()
                .csrf();
    }
}

启动程序进行测试,测试时候账号必须是数据库存在的,然后尝试失败3次,账号即被锁定

时间: 2024-10-05 01:53:03

spring security实现限制登录次数功能的相关文章

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

今天在前面一节的基础之上,再增加一点新内容,默认情况下Spring Security不会对登录错误的尝试次数做限制,也就是说允许暴力尝试,这显然不够安全,下面的内容将带着大家一起学习如何限制登录尝试次数. 首先对之前创建的数据库表做点小调整 一.表结构调整 T_USERS增加了如下3个字段: D_ACCOUNTNONEXPIRED,NUMBER(1) -- 表示帐号是否未过期D_ACCOUNTNONLOCKED,NUMBER(1), -- 表示帐号是否未锁定D_CREDENTIALSNONEXP

spring security使用自定义登录界面后,不能返回到之前的请求界面的问题

昨天因为集成spring security oauth2,所以对之前spring security的配置进行了一些修改,然后就导致登录后不能正确跳转回被拦截的页面,而是返回到localhost根目录. 开始以为是被oauth2拦截了导致出了问题,但是security的拦截器是优先起作用的,所以这不可能. 最后实在没法只有打断点调试找毛病(下图为spring security登录后重定向至拦截前访问的url的实现原理流程) 图片是在这里看到的https://blog.csdn.net/zy_coo

Spring security 4.1 登录成功后重复进行认证问题

问题场景: 登录成功后,在执行某个功能操作(例如:系统管理模块的删除功能时),会去执行UserDetailsService.loadUserByUsername 再次进行用户认证. 出现问题版本 Spring security 4.04  . 4.10 通过源码分析发现BasicAuthenticationFilter.authenticationIsRequired(username) 一直返回true (true表示需要认证) org.springframework.security.web

Spring Security 4 自定义登录表单 注解和XML例子(带源码)

上一篇文章: Spring Security 4 Hello World 基于注解 和 XML 例子 下一篇:Spring Security 4 退出 示例 原文地址:http://websystique.com/spring-security/spring-security-4-custom-login-form-annotation-example/ [已翻译文章,点击分类里面的spring security 4查看.] [ 翻译by 明明如月 QQ 605283073] 本文演示Sprin

spring boot系列--spring security (基于数据库)登录和权限控制

先说一下AuthConfig.java Spring Security的主要配置文件之一 AuthConfig 1 @Configuration 2 @EnableWebSecurity 3 public class AuthConfig extends WebSecurityConfigurerAdapter { 4 @Override 5 protected void configure(HttpSecurity httpSecurity) throws Exception { 6 http

Spring Security(十):3. What’s New in Spring Security 4.2 (新功能)

Among other things, Spring Security 4.2 brings early support for Spring Framework 5. You can find the change logs for 4.2.0.M1, 4.2.0.RC1, 4.2.0.RELEASE which closes over 80 issues. The overwhelming majority of these features were contributed by the

Spring Security在标准登录表单中添加一个额外的字段

概述 在本文中,我们将通过向标准登录表单添加额外字段来实现Spring Security的自定义身份验证方案. 我们将重点关注两种不同的方法,以展示框架的多功能性以及我们可以使用它的灵活方式. 我们的第一种方法是一个简单的解决方案,专注于重用现有的核心Spring Security实现. 我们的第二种方法是更加定制的解决方案,可能更适合高级用例. 2. Maven设置 我们将使用Spring Boot启动程序来引导我们的项目并引入所有必需的依赖项. 我们将使用的设置需要父声明,Web启动器和安全

Spring实战 - 实现自动登录的功能

环境: MacOS + IntelliJ IDEA 2019.3.1 (Ultimate Edition) Cookie:存储在用户本地终端上的数据.Cookie 中每条Cookie的存储空间4k LocalStorage:用于本地存储,解决了Cookie存储空间不足的问题,LocalStorage 中一般浏览器支持的是5M大小.LocalStorage为永久存储. SessionStorage:当会话结束时,SessionStorage中的键值对就会被清空. Flash Cookie:可跨浏览

为什么Spring Security看不见登录失败或者注销的提示

有很多人在利用Spring Security进行角色权限设计开发时,一般发现正常登录时没问题,但是注销.或者用户名时,直接就回到登录页面了,在登录页面上看不见任何提示信息,如"用户名/密码有误"或"注销成功". 那么如何做呢?很简单. 1.自定义安全配置类(继承自WebSecurityConfigurerAdapter) 在我们的自定义安全配置类中,需要做必要的设置,如下图: 上图中标红框的部分很关键: failureUrl("/login?error=t