Рубрики
Без рубрики

Повышение безопасности Spring с помощью двухфакторной аутентификации

Двухфакторная аутентификация добавляет дополнительный уровень безопасности вашему веб-приложению, запрашивая пользователей об этом… Помеченный java, spring, nexmo, security.

Двухфакторная аутентификация добавляет дополнительный уровень безопасности вашему веб-приложению, запрашивая у пользователей вторую форму идентификации. Общие вторые факторы включают:

  • Коды аутентификатора
  • Биометрия
  • Коды электронной почты или текстовых сообщений

Давайте рассмотрим, как вы можете добавить двухфакторную аутентификацию в существующее веб-приложение с помощью Nexmo.

Для того, чтобы следовать вместе с учебным пособием, вам понадобится следующее:

Клонируйте ветку начало работы .

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 чтобы ознакомиться с образцом приложения.

Там три страницы:

Когда пользователи входят в систему, нашим единственным критерием приемлемости является то, что они указали имя пользователя и пароль. Что, если эта информация была украдена? Что такое то, что мы могли бы использовать, что физически находится рядом с пользователем?

Есть кое-что, что, я гарантирую, почти у 90% из вас и наших пользователей есть в пределах досягаемости руки. Мобильный телефон.

Вот как это будет работать:

  1. Пользователь войдет в ваше приложение, как обычно.
  2. Им будет предложено ввести четырехзначный проверочный код.
  3. Одновременно на номер телефона в их аккаунте будет отправлен четырехзначный проверочный код. Если у них нет номера телефона в их учетной записи, мы позволим им обойти двухфакторную аутентификацию.
  4. Введенный ими код будет проверен, чтобы убедиться, что это тот же код, который мы им отправили.

Мы собираемся использовать 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 getAuthorities() {
    Set authorities = 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 {
        Optional matches = 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());
    List authorities = 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

A text message has been sent to your mobile device. Please enter the code below:

Нам также нужен контроллер для предоставления страницы пользователю. Создайте Контроллер проверки в пакете 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”