Двухфакторная аутентификация добавляет дополнительный уровень безопасности вашему веб-приложению, запрашивая у пользователей вторую форму идентификации. Общие вторые факторы включают:
- Коды аутентификатора
- Биометрия
- Коды электронной почты или текстовых сообщений
Давайте рассмотрим, как вы можете добавить двухфакторную аутентификацию в существующее веб-приложение с помощью Nexmo.
Для того, чтобы следовать вместе с учебным пособием, вам понадобится следующее:
- Общее понимание Java и корпоративных Java-технологий
- Java SDK , установленный на вашем компьютере
- A Учетная запись разработчика Nexmo вместе с ключом API и секретом
- Клон ветки
начало работы
на GitHub
Клонируйте ветку начало работы
.
git clone https://github.com/cr0wst/demo-twofactor.git -b getting-started cd demo-twofactor
Пример приложения построен с использованием Spring Boot . Если в вашей системе установлен Gradle , вы должны иметь возможность выполнить задачу bootRun
для запуска приложения.
Если нет, то не беспокойтесь ; репозиторий содержит оболочку Gradle, которая по-прежнему позволит вам выполнять задачи.
./gradlew bootRun
Это позволит загрузить все зависимости, скомпилировать приложение и запустить встроенный сервер.
Как только сервер будет запущен, вы сможете перейти к http://localhost:8080 чтобы ознакомиться с образцом приложения.
Там три страницы:
- Домашняя страница |/- доступна всем. Страница
- входа в систему – позволяет пользователям вводить имя пользователя и пароль (по умолчанию используется demo
/
демо).
- Секретная страница – доступна только пользователям с ролью . ПОЛЬЗОВАТЕЛИ
роль.
Когда пользователи входят в систему, нашим единственным критерием приемлемости является то, что они указали имя пользователя и пароль. Что, если эта информация была украдена? Что такое то, что мы могли бы использовать, что физически находится рядом с пользователем?
Есть кое-что, что, я гарантирую, почти у 90% из вас и наших пользователей есть в пределах досягаемости руки. Мобильный телефон.
Вот как это будет работать:
- Пользователь войдет в ваше приложение, как обычно.
- Им будет предложено ввести четырехзначный проверочный код.
- Одновременно на номер телефона в их аккаунте будет отправлен четырехзначный проверочный код. Если у них нет номера телефона в их учетной записи, мы позволим им обойти двухфакторную аутентификацию.
- Введенный ими код будет проверен, чтобы убедиться, что это тот же код, который мы им отправили.
Мы собираемся использовать Nexmo Verify API для генерации кода и проверки того, является ли введенный ими код действительным.
Создание новой роли
Первым шагом будет создание новой роли. Эта роль будет использоваться для удержания аутентифицированного пользователя в состоянии чистилища до тех пор, пока мы не подтвердим его личность.
Добавьте роль PRE_VERIFICATION_USER
в перечисление Role
.
// src/main/net/smcrow/demo/twofactor/user/Role.java public enum Role implements GrantedAuthority { USER, PRE_VERIFICATION_USER; // ... }
Для того, чтобы она была применена в качестве роли по умолчанию, нам необходимо обновить метод getAuthorities()
класса Standard User Details
.
// src/main/net/smcrow/demo/twofactor/user/StandardUserDetails.java @Override public Collection extends GrantedAuthority> getAuthorities() { Setauthorities = new HashSet<>(); authorities.add(Role.PRE_VERIFICATION_USER); return authorities; }
Обработка Проверочной Информации
Nexmo предоставит нам идентификатор запроса , который будет использоваться при подтверждении кода, предоставленного пользователем. Существует множество способов, которыми мы можем хранить эту информацию. В этом руководстве мы будем сохранять его в базе данных.
Хранение Проверочной Информации
Сначала создайте класс Verification
в пакете verify
.
// src/main/net/smcrow/demo/twofactor/verify/Verification.java @Entity public class Verification { @Id @Column(unique = true, nullable = false) private String phone; @Column(nullable = false) private String requestId; @Column(nullable = false) private Date expirationDate; @PersistenceConstructor public Verification() { // Empty constructor for JPA } public Verification(String phone, String requestId, Date expirationDate) { this.phone = phone; this.requestId = requestId; this.expirationDate = expirationDate; } // ... Getters and Setters }
Обратите внимание, мы также храним Срок годности
. По умолчанию запросы API проверки действительны только в течение пяти минут. Они будут удалены из таблицы, когда либо:
- Пользователь успешно подтвердил свою личность.
- У них истек срок годности.
Мы будем использовать Spring scheduler для их периодической очистки.
Работа с Проверочной информацией
Создайте репозиторий Проверки
интерфейс в пакете verify
.
// src/mainnet/smcrow/demo/twofactor/verify/VerificationRepository.java @Repository public interface VerificationRepository extends JpaRepository{ Optional findByPhone(String phone); void deleteByExpirationDateBefore(Date date); }
Удаление Просроченных Запросов
В пакете two factor
создайте следующий класс конфигурации.
// src/main/net/smcrow/demo/twofactor/ScheduleConfiguration.java @Configuration @EnableScheduling public class ScheduleConfiguration { @Autowired private VerificationRepository verificationRepository; @Scheduled(fixedDelay = 1000) @Transactional public void purgeExpiredVerifications() { verificationRepository.deleteByExpirationDateBefore(new Date()); } }
Это настроит запланированную команду, которая будет выполняться каждую секунду, которая будет запрашивать любые истекшие Проверка
сущностей и удаление их.
Настройка клиента Nexmo
Мы будем использовать клиент nexmo-java для взаимодействия с Nexmo.
Объявить зависимость
Сначала объявите следующую зависимость в файле build.gradle
.
dependencies { // .. other dependencies compile('com.nexmo:client:3.3.0') }
Предоставлять Информацию
Теперь определите следующую информацию в файле application.properties
.
# Add your nexmo credentials nexmo.api.key=your-api-key nexmo.api.secret=your-api-secret
Определите бобы
Далее мы собираемся определить NexmoClient
и Проверить клиента
как бобы. Это позволит Spring внедрить их в качестве зависимостей в нашу службу проверки Nexmo
.
Добавьте следующие определения в класс Двухфакторное приложение
.
// src/main/net/smcrow/demo/twofactor/TwofactorApplication.java @Bean public NexmoClient nexmoClient(Environment environment) { AuthMethod auth = new TokenAuthMethod( environment.getProperty("nexmo.api.key"), environment.getProperty("nexmo.api.secret") ); return new NexmoClient(auth); } @Bean public VerifyClient nexmoVerifyClient(NexmoClient nexmoClient) { return nexmoClient.getVerifyClient(); }
Создайте службу проверки Nexmo
Мы собираемся создать сервис, который позволит нам поручать клиенту делать запросы.
Добавьте Службу проверки Nexmo/| в пакет
verify .
// src/main/net/smcrow/demo/twofactor/verify/NexmoVerificationService.java @Service public class NexmoVerificationService { private static final String APPLICATION_BRAND = "2FA Demo"; private static final int EXPIRATION_INTERVALS = Calendar.MINUTE; private static final int EXPIRATION_INCREMENT = 5; @Autowired private VerificationRepository verificationRepository; @Autowired private UserRepository userRepository; @Autowired private VerifyClient verifyClient; public Verification requestVerification(String phone) throws VerificationRequestFailedException { Optionalmatches = verificationRepository.findByPhone(phone); if (matches.isPresent()) { return matches.get(); } return generateAndSaveNewVerification(phone); } public boolean verify(String phone, String code) throws VerificationRequestFailedException { try { Verification verification = retrieveVerification(phone); if (verifyClient.check(verification.getRequestId(), code).getStatus() == 0) { verificationRepository.delete(phone); return true; } return false; } catch (VerificationNotFoundException e) { requestVerification(phone); return false; } catch (IOException | NexmoClientException e) { throw new VerificationRequestFailedException(e); } } private Verification retrieveVerification(String phone) throws VerificationNotFoundException { Optional matches = verificationRepository.findByPhone(phone); if (matches.isPresent()) { return matches.get(); } throw new VerificationNotFoundException(); } private Verification generateAndSaveNewVerification(String phone) throws VerificationRequestFailedException { try { VerifyResult result = verifyClient.verify(phone, APPLICATION_BRAND); if (StringUtils.isBlank(result.getErrorText())) { String requestId = result.getRequestId(); Calendar now = Calendar.getInstance(); now.add(EXPIRATION_INTERVALS, EXPIRATION_INCREMENT); Verification verification = new Verification(phone, requestId, now.getTime()); return verificationRepository.save(verification); } } catch (IOException | NexmoClientException e) { throw new VerificationRequestFailedException(e); } throw new VerificationRequestFailedException(); } }
В этом классе есть два основных метода:
запросить проверку
который используется, ну, для запроса проверки.verify
который используется для проверки предоставленного кода, предоставленного пользователем.
Метод Проверки запроса
Метод сначала проверяет, есть ли у нас уже ожидающий подтверждения запрос на номер телефона пользователя. Это позволяет нам отправлять тот же идентификатор запроса пользователю, если он попытается снова войти в приложение.
Если никакой предварительной проверки нет, то запрашивается новый проверочный код, который сохраняется в базе данных. Если по какой-либо причине мы не можем назначить им новый код, генерируется исключение Ошибка запроса на проверку
.
Добавьте это исключение в пакет verify
.
// src/main/net/smcrow/demo/twofactor/verify/VerificationRequestFailedException.java public class VerificationRequestFailedException extends Throwable { public VerificationRequestFailedException() { this("Failed to verify request."); } public VerificationRequestFailedException(String message) { super(message); } public VerificationRequestFailedException(Throwable cause) { super(cause); } }
Метод проверки
Метод verify
отправляет идентификатор запроса и код в Nexmo для проверки. Nexmo возвращает нулевой статус, если проверка прошла успешно. При успешной проверке объект Verification
удаляется из базы данных и возвращается true
.
Если мы не смогли найти объект Verification
, возможно, срок его действия истек, мы запрашиваем новый объект и возвращаем false. Если возникают какие-либо проблемы с проверкой, мы выдаем Ошибка запроса на проверку
.
Метод retrieve Verification
выдаст исключение Verification NotFoundException
, если Проверка
не была найдена.
Добавьте это исключение в пакет verify
.
// src/main/net/smcrow/demo/twofactor/verify/VerificationNotFoundException.java public class VerificationNotFoundException extends Throwable { public VerificationNotFoundException() { this("Failed to find verification."); } public VerificationNotFoundException(String message) { super(message); } }
Использование службы верификации Nexmo
Мы собираемся использовать сервис как для отправки кода, так и для проверки кода. Отправка кода выполняется после успешной аутентификации.
Запуск запроса на проверку
Давайте реализуем пользовательский AuthenticationSuccessHandler
, который будет вызван после успешной аутентификации пользователя.
Добавьте Обработчик успешной двухфакторной аутентификации
в пакет verify
.
// src/main/net/smcrow/demo/twofactor/verify/TwoFactorAuthenticationSuccessHandler.java @Component public class TwoFactorAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private static final String VERIFICATION_URL = "/verify"; private static final String INDEX_URL = "/"; @Autowired private NexmoVerificationService verificationService; @Autowired private UserRepository userRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String phone = ((StandardUserDetails) authentication.getPrincipal()).getUser().getPhone(); if (phone == null || !requestAndRegisterVerification(phone)) { bypassVerification(request, response, authentication); return; } new DefaultRedirectStrategy().sendRedirect(request, response, VERIFICATION_URL); } private boolean requestAndRegisterVerification(String phone) { try { return verificationService.requestVerification(phone) != null; } catch (VerificationRequestFailedException e) { return false; } } private void bypassVerification(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { verificationService.updateAuthentication(authentication); new DefaultRedirectStrategy().sendRedirect(request, response, INDEX_URL); } }
Когда пользователь успешно прошел аутентификацию, мы проверяем, есть ли у него номер телефона.
Если у них есть номер телефона, на их устройство отправляется код. Если у них нет номера телефона или мы не можем отправить код, мы разрешаем им обойти проверку.
Метод обхода проверки
основан на методе проверки подлинности обновления
службы NexmoVerificationService
.
Добавьте это в службу проверки Nexmo/|:
// src/main/net/smcrow/demo/twofactor/verify/NexmoVerificationService.java public void updateAuthentication(Authentication authentication) { Role role = retrieveRoleFromDatabase(authentication.getName()); Listauthorities = new ArrayList<>(); authorities.add(role); Authentication newAuthentication = new UsernamePasswordAuthenticationToken( authentication.getPrincipal(), authentication.getCredentials(), authorities ); SecurityContextHolder.getContext().setAuthentication(newAuthentication); } private Role retrieveRoleFromDatabase(String username) { Optional match = userRepository.findByUsername(username); if (match.isPresent()) { return match.get().getRole(); } throw new UsernameNotFoundException("Username not found."); }
Этот метод используется для назначения роли
, определенной в базе данных, текущему пользователю и удаляет роль PRE_VERIFICATION_USER
.
Запрашивает у Пользователя код
Как только пользователю будет отправлен код, он перенаправляется на страницу verification . Давайте поработаем над созданием этой страницы дальше.
Создайте новый HTML-файл с именем verify.html
в каталоге ресурсы/шаблоны
.
Two Factor Authorization Demo There was an error with your login.
Нам также нужен контроллер для предоставления страницы пользователю. Создайте Контроллер проверки
в пакете verify
.
// src/main/net/smcrow/demo/twofactor/verify/VerificationController.java @Controller public class VerificationController { @Autowired private NexmoVerificationService verificationService; @PreAuthorize("hasRole('PRE_VERIFICATION_USER')") @GetMapping("/verify") public String index() { return "verify"; } @PreAuthorize("hasRole('PRE_VERIFICATION_USER')") @PostMapping("/verify") public String verify(@RequestParam("code") String code, Authentication authentication) { User user = ((StandardUserDetails) authentication.getPrincipal()).getUser(); try { if (verificationService.verify(user.getPhone(), code)) { verificationService.updateAuthentication(authentication); return "redirect:/"; } return "redirect:verify?error"; } catch (VerificationRequestFailedException e) { // Having issues generating keys let them through. verificationService.updateAuthentication(authentication); return "redirect:/"; } } }
Этот контроллер обслуживает страницу проверки с помощью метода index
и обрабатывает отправку формы с помощью метода verify
.
Эта страница доступна только для пользователей с ролью PRE_VERIFICATION_USER
. При успешной проверке метод update Authentication
снова используется для замены этой роли на их сохраненную.
Завершение цепочки проверки
Последним шагом является обновление Конфигурации безопасности приложения
для использования нашего Обработчика успешной двухфакторной аутентификации
.
Измените Конфигурацию безопасности приложения
для подключения вашего обработчика и используйте его с помощью метода successHandler
.
// src/main/net/smcrow/demo/twofactor/AppSecurityConfiguration.java @Autowired private TwoFactorAuthenticationSuccessHandler twoFactorAuthenticationSuccessHandler; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { // Webjar resources httpSecurity.authorizeRequests().antMatchers("/webjars/**").permitAll() .and().formLogin().loginPage("/login").permitAll() .successHandler(twoFactorAuthenticationSuccessHandler) .and().logout().permitAll(); }
Попробуйте это!
Вам нужно будет добавить свой номер телефона в файл data.sql
.
Мы не собираемся проводить какую-либо проверку номера телефона, и он должен быть в формате E.164 .
INSERT INTO user (username, password, role, phone) VALUES ('phone', 'phone', 'USER', 15555555555);
Теперь вы должны быть готовы и работать. Загрузите приложение и попробуйте войти в систему. Предполагая, что ваш ключ API, секрет API и введенный номер телефона верны; вы должны получить текстовое сообщение с четырехзначным кодом.
Что Мы Сделали?
Мы сделали много вещей.
Короче говоря, мы внедрили двухфакторную аутентификацию, чтобы лучше защитить наше приложение. Мы сделали это с помощью:
- Создание пользовательского
AuthenticationSuccessHandler
для перенаправления пользователя на страницу проверки после предоставления ему кода. - Используя библиотеку
nexmo-java
, обернув ее в службу проверкиNexmo/| , для отправки кодов подтверждения нашим пользователям.
Использование преимуществ планировщика Spring для удаления кодов подтверждения с истекшим сроком действия. - Создание страницы, на которой пользователь может ввести свой проверочный код.
Ознакомьтесь с окончательным кодом из этого руководства на GitHub.
Заглядывая в Будущее
Существуют различные способы реализации двухфакторной аутентификации. Если вам интересно узнать о каких-либо фреймворках и технологиях, используемых в примере кода, вот краткое описание:
Не забывай об этом вы можете быть участником Nexmo для клиента nexmo-java .
Оригинал: “https://dev.to/cr0wst/beefing-up-your-spring-security-with-two-factor-authentication-4m5p”