Автор оригинала: mkyong.
В этом уроке мы покажем вам, как ограничить попытки входа в систему в Spring Security, что означает, что если пользователь попытается войти в систему с неверным паролем более 3 раз, система заблокирует пользователя и сделает его больше неспособным войти в систему.
Используемые технологии и инструменты:
- Пружина 3.2.8.ОТПУСТИТЕ
- Пружинная защита 3.2.3.ВЫПУСК
- Весенний JDBC 3.2.3.РЕЛИЗ
- Затмение 4.2
- JDK 1.6
- Мавен 3
- Сервер MySQL 5.6
- Tomcat 7 (сервлет 3.x)
Несколько кратких заметок для этого урока:
- Будет использоваться база данных MySQL.
- Это пример на основе аннотаций безопасности Spring.
- Создайте таблицу “пользователи” со столбцом “Учетная запись заблокирована”.
- Создайте таблицу “user_attempts” для хранения недопустимых попыток входа в систему.
- Будет использоваться весенний JDBC.
- Отображение пользовательских сообщений об ошибках на основе возвращенного исключения.
- Создайте настраиваемого “поставщика проверки подлинности”
1. Решение
Просмотрите существующий класс аутентификации Spring Security, функция “заблокировано” уже реализована. Чтобы включить ограничение попыток входа в систему, вам необходимо установить Сведения о пользователе.isaccountnonlocked
в значение false.
package org.springframework.security.authentication.dao; public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { //... }
package org.springframework.security.authentication.dao; public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { private class DefaultPreAuthenticationChecks implements UserDetailsChecker { public void check(UserDetails user) { if (!user.isAccountNonLocked()) { logger.debug("User account is locked"); throw new LockedException( messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"), user); } //... } }
2. Демонстрация проекта
3. Каталог проектов
Просмотрите окончательную структуру проекта (на основе аннотаций):
4. База данных
Вот сценарии MySQL для создания таблиц users, user_roles и user_attempts.
4.1 Создайте таблицу “пользователи” со столбцом “Учетная запись заблокирована”.
CREATE TABLE users ( username VARCHAR(45) NOT NULL , password VARCHAR(45) NOT NULL , enabled TINYINT NOT NULL DEFAULT 1 , accountNonExpired TINYINT NOT NULL DEFAULT 1 , accountNonLocked TINYINT NOT NULL DEFAULT 1 , credentialsNonExpired TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (username));
4.2 Создайте таблицу “роли пользователя”.
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));
4.3 Создайте таблицу “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 NOT NULL, PRIMARY KEY (id) );
4.4 Вводит пользователя для тестирования.
INSERT INTO users(username,password,enabled) VALUES ('mkyong','123456', true); INSERT INTO user_roles (username, role) VALUES ('mkyong', 'ROLE_USER'); INSERT INTO user_roles (username, role) VALUES ('mkyong', 'ROLE_ADMIN');
5. Классы Попыток пользователя
Этот класс представляет данные таблицы “user_attempts”.
package com.mkyong.users.model; import java.util.Date; public class UserAttempts { private int id; private String username; private int attempts; private Date lastModified; //getter and setter }
6. Классы DAO
Класс DAO для обновления недопустимых попыток входа в систему, прочитайте комментарии для пояснения.
package com.mkyong.users.dao; import com.mkyong.users.model.UserAttempts; public interface UserDetailsDao { void updateFailAttempts(String username); void resetFailAttempts(String username); UserAttempts getUserAttempts(String username); }
package com.mkyong.users.dao; 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.security.authentication.LockedException; import org.springframework.stereotype.Repository; import com.mkyong.users.model.UserAttempts; @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; @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 UserAttempts getUserAttempts(String username) { try { UserAttempts userAttempts = getJdbcTemplate().queryForObject(SQL_USER_ATTEMPTS_GET, new Object[] { username }, new RowMapper() { 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; } } @Override public void resetFailAttempts(String username) { getJdbcTemplate().update( SQL_USER_ATTEMPTS_RESET_ATTEMPTS, new Object[] { 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; } }
7. Служба пользовательских данных
По умолчанию JdbcDaoImpl
всегда будет устанавливать accountNonLocked
значение true, а это не то, что мы хотим. Просмотрите исходный код.
package org.springframework.security.core.userdetails.jdbc; public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService { //... protected ListloadUsersByUsername(String username) { return getJdbcTemplate().query(usersByUsernameQuery, new String[] {username}, new RowMapper () { 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); } }); }
Чтобы сэкономить время разработки, вы можете расширить JdbcDaoImpl
и переопределить оба имя_пользователя
и createUserDetails
для получения настроенных Сведений о пользователе
.
package com.mkyong.users.service; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import javax.annotation.PostConstruct; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; 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; /** * Reference org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl * * @author mkyong * */ @Service("userDetailsService") public class CustomUserDetailsService extends JdbcDaoImpl { @Autowired 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 to get accountNonLocked @Override public ListloadUsersByUsername(String username) { return getJdbcTemplate().query(super.getUsersByUsernameQuery(), new String[] { username }, new RowMapper () { 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 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); } }
8. Поставщик Daoauthentication
Создайте настраиваемого поставщика проверки подлинности, для каждой недопустимой попытки входа в систему обновите таблицу user_attempts и выведите Исключение LockedException
если будет выполнено максимальное количество попыток.
package com.mkyong.web.handler; import java.util.Date; 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 com.mkyong.users.dao.UserDetailsDao; import com.mkyong.users.model.UserAttempts; @Component("authenticationProvider") public class LimitLoginAuthenticationProvider extends DaoAuthenticationProvider { @Autowired 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); //if reach here, means login success, else an exception will be thrown //reset the user_attempts userDetailsDao.resetFailAttempts(authentication.getName()); return auth; } catch (BadCredentialsException e) { //invalid login, update to user_attempts userDetailsDao.updateFailAttempts(authentication.getName()); throw e; } catch (LockedException e){ //this user is locked! String error = ""; UserAttempts userAttempts = userDetailsDao.getUserAttempts(authentication.getName()); if(userAttempts!=null){ Date lastAttempts = userAttempts.getLastModified(); error = "User account is locked!
Username : " + authentication.getName() + "
Last Attempts : " + lastAttempts; }else{ error = e.getMessage(); } throw new LockedException(error); } } }
9. Пружинный контроллер
Стандартный класс контроллера, обратитесь к методу login
, он покажет вам, как играть со значением сеанса – ” SPRING_SECURITY_LAST_EXCEPTION ” и настроить сообщение об ошибке.
package com.mkyong.web.controller; import javax.servlet.http.HttpServletRequest; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; @Controller public class MainController { @RequestMapping(value = { "/", "/welcome**" }, method = RequestMethod.GET) public ModelAndView defaultPage() { ModelAndView model = new ModelAndView(); model.addObject("title", "Spring Security Limit Login - Annotation"); model.addObject("message", "This is default page!"); model.setViewName("hello"); return model; } @RequestMapping(value = "/admin**", method = RequestMethod.GET) public ModelAndView adminPage() { ModelAndView model = new ModelAndView(); model.addObject("title", "Spring Security Limit Login - Annotation"); model.addObject("message", "This page is for ROLE_ADMIN only!"); model.setViewName("admin"); return model; } @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", "You've been logged out successfully."); } model.setViewName("login"); return model; } //customize the error message private String getErrorMessage(HttpServletRequest request, String key){ Exception exception = (Exception) request.getSession().getAttribute(key); String error = ""; if (exception instanceof BadCredentialsException) { error = "Invalid username and password!"; }else if(exception instanceof LockedException) { error = exception.getMessage(); }else{ error = "Invalid username and password!"; } return error; } // for 403 access denied page @RequestMapping(value = "/403", method = RequestMethod.GET) public ModelAndView accesssDenied() { ModelAndView model = new ModelAndView(); // check if user is login Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (!(auth instanceof AnonymousAuthenticationToken)) { UserDetails userDetail = (UserDetails) auth.getPrincipal(); System.out.println(userDetail); model.addObject("username", userDetail.getUsername()); } model.setViewName("403"); return model; } }
10. Конфигурация безопасности Spring
Прикрепил свой настроенный поставщик аутентификации
.
package com.mkyong.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; 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; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("authenticationProvider") AuthenticationProvider authenticationProvider; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/admin/**") .access("hasRole('ROLE_USER')").and().formLogin() .loginPage("/login").failureUrl("/login?error") .usernameParameter("username") .passwordParameter("password") .and().logout().logoutSuccessUrl("/login?logout").and().csrf(); } }
Сделано.
11. Демонстрация
Демонстрационная страница – http://localhost:8080/spring-security-limit-login-annotation/admin
11.1, При первых неверных попытках входа в систему будет отображаться обычное сообщение об ошибке.
11.2, Если будет выполнено максимальное количество неверных попыток входа в систему, появится сообщение об ошибке “Учетная запись пользователя заблокирована”.
11.3, Если пользователь находится в статусе “заблокирован”, и все еще пытается войти в систему снова. Отобразится заблокированная информация.
11.4 Просмотрите таблицу “пользователи”, если или значение false означает, что этот пользователь находится в заблокированном состоянии.
Скачать Исходный Код
Рекомендации
- StackOverflow: Как я могу ограничить попытки входа в систему в Spring Security?
- Весенняя Официальная справка: Основные услуги – Поставщик услуг аутентификации
- Пример аннотации Пользовательской формы входа Spring Security
- Весенний DaoAuthenticationProvider javadoc
- Весна JdbcDaoImpl javadoc
Оригинал: “https://mkyong.com/spring-security/spring-security-limit-login-attempts-example/”