本节是在基于注解方式进行的,后面的例子都会基于注解形式,不再实现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. Android开发:《Gradle Recipes for Android》阅读笔记(翻译)4.4——自定义代码集合

    问题: 你想要在项目中使用非标准的代码目录. 解决方案: 在gradle的build配置里面使用sourceSets属性. 讨论: Android分发的samples里面使用多个代码目录,使得通用的文 ...

  2. Codeforces Round #207 (Div. 1) B (gcd的巧妙运用)

    比赛的时候不知道怎么写... 太弱了. 看了别人的代码,觉得这个是个经典的知识点吧. gcd的巧妙运用 自己想的时候苦苦思考怎么用dp求解. 无奈字符串太长而想不出好的算法. 其实在把a和b字符串都分 ...

  3. tfs+git

    TFS+GIT 一:背景介绍 技术团队的代码管理工具原来使用的是纯TFS方案,使用两年后发现一些问题:体积太大,每次新建一个分支需要本地下载一份代码:操作不便,功能分支的建立.合并不方便,本地有很多同 ...

  4. 因为td设置relative导致td的border问题

    在ff下因为td设置relative导致td的border问题:其实是个老问题了~碰到了拿出来记录下 td 中添加如下样式 background-clip: padding-box

  5. SetForegroundWindow以及 如何将一个某个窗口提到最顶层(转)

    http://hi.baidu.com/gookings/item/2b7912ca8d5b3625a0b50aa2 SetForegroundWindow 函数功能:该函数将创建指定窗口的线程设置到 ...

  6. Alcor(安国)AU6387量产修复(u盘修复)

    2010年买的U盘,自从去年坏掉一直没有用. 今天试着把它修理的心态,看看能修好不能.不料真的被我搞好了. 下面是教程链接 如果你的芯片跟我的一样,我人品保证你可以成功. 如果你看教程之后量产 成功, ...

  7. ZOJ 3941 Kpop Music Party(省赛, 贪心)

    Kpop Music Party Time Limit: 2 Seconds      Memory Limit: 65536 KB Marjar University often hosts Kpo ...

  8. CentOS安装Apache-2.4.25+安全配置

    注:以下所有操作均在CentOS 6.5 x86_64位系统下完成. #准备工作# 在安装Nginx之前,请确保已经使用yum安装了各基础组件,并且配置了www用户和用户组,具体见<CentOS ...

  9. Linux内核协议栈相关资料链接

    1.Linux内核网络分析(个人博客集合,分析较清晰) http://blog.csdn.net/qy532846454/article/category/1385933/2

  10. linux命令行与shell脚本编程 -----15控制脚本

    常见的Linux系统信号 信号 值 描述 1 SIGHUP 挂起进程 2 SIGINT 终止进程 3 SIGQUIT 停止进程 9 SIGKILL 无条件终止进程 15 SIGTERM 可能的话终止进 ...