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次,账号即被锁定