本节是在基于注解方式进行的,后面的例子都会基于注解形式,不再实现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','', true);
INSERT INTO users(username,password,enabled) VALUES ('wpp','', 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次,账号即被锁定

 

 

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

  1. Spring Security 的注册登录流程

    Spring Security 的注册登录流程 数据库字段设计 主要数据库字段要有: 用户的 ID 用户名称 联系电话 登录密码(非明文) UserDTO对象 需要一个数据传输对象来将所有注册信息发送 ...

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

    昨天因为集成spring security oauth2,所以对之前spring security的配置进行了一些修改,然后就导致登录后不能正确跳转回被拦截的页面,而是返回到localhost根目录. ...

  3. spring security之 默认登录页源码跟踪

    spring security之 默认登录页源码跟踪 ​ 2021年的最后2个月,立个flag,要把Spring Security和Spring Security OAuth2的应用及主流程源码研究透 ...

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

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

  5. spring boot:spring security给用户登录增加自动登录及图形验证码功能(spring boot 2.3.1)

    一,图形验证码的用途? 1,什么是图形验证码? 验证码(CAPTCHA)是"Completely Automated Public Turing test to tell Computers ...

  6. spring security采用自定义登录页和退出功能

    更新... 首先采用的是XML配置方式,请先查看  初识Spring security-添加security 在之前的示例中进行代码修改 项目结构如下: 一.修改spring-security.xml ...

  7. Spring Security(12)——Remember-Me功能

    目录 1.1     概述 1.2     基于简单加密token的方法 1.3     基于持久化token的方法 1.4     Remember-Me相关接口和实现类 1.4.1    Toke ...

  8. Spring Security 入门(1-3-3)Spring Security - logout 退出登录

    要实现退出登录的功能我们需要在 http 元素下定义 logout 元素,这样 Spring Security 将自动为我们添加用于处理退出登录的过滤器 LogoutFilter 到 FilterCh ...

  9. Spring Security Oauth2 单点登录案例实现和执行流程剖析

    Spring Security Oauth2 OAuth是一个关于授权的开放网络标准,在全世界得到的广泛的应用,目前是2.0的版本.OAuth2在“客户端”与“服务提供商”之间,设置了一个授权层(au ...

随机推荐

  1. 【BZOJ4773】负环 倍增Floyd

    [BZOJ4773]负环 Description 在忘记考虑负环之后,黎瑟的算法又出错了.对于边带权的有向图 G = (V, E),请找出一个点数最小的环,使得 环上的边权和为负数.保证图中不包含重边 ...

  2. APP测试瞎话

    APP测试        一.功能性        1.APP的安装.卸载        2.APP中业务功能            二.性能测试        1.高.中.低端机上运行效果      ...

  3. UVAlive 7041 The Problem to Slow Down You(回文树)

    题目链接: https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show ...

  4. CodeForces 157B Trace

    B. Trace time limit per test 2 seconds memory limit per test 256 megabytes input standard input outp ...

  5. mysqldump迁移说明

    使用mysqldump导出数据, 数据包含单行insert,带字段值 #使用mysqldump备份数据到文件, 主要在每个分片的主上面进行备份,确保数据是最新的. mysqldump -h192. - ...

  6. 更新设置api

    8.8 更新设置 API Elasticsearch允许在elasticsearch.yml文件中指定各种参数来调优.但你应该把这个文件当做 默认设置,可以在运行时通过Elasticsearch RE ...

  7. yum -y install epel-release

    EPEL - Fedora Project Wiki https://fedoraproject.org/wiki/EPEL

  8. HTTP 常见状态码

    1. 以"1"开头(临时响应) 100: Continue,请求者应当继续提出请求;表示服务端已经收到请求的一部分,正在等待其余部分; 101: Switching Protoco ...

  9. 我的Android进阶之旅------>解决:debug-stripped.ap_' specified for property 'resourceFile' does not exist.

    1.错误描述 更新Android Studio到2.0版本后,出现了编译失败的问题,我clean project然后重新编译还是出现抑郁的问题,问题具体描述如下所示: Error:A problem ...

  10. BLOG总结

    1.登录:http://www.cnblogs.com/shaojiafeng/p/7868195.html 2.注册 - urls -前端页面中写 username ,password,passwo ...