Перейти к основному содержимому

Двухфакторная аутентификация с Spring Security

· 5 мин. чтения

1. Обзор

В этом руководстве мы собираемся реализовать функциональность двухфакторной аутентификации с помощью Soft Token и Spring Security.

Мы собираемся добавить новую функциональность в существующий простой процесс входа в систему и использовать приложение Google Authenticator для создания токенов.

Проще говоря, двухфакторная аутентификация — это процесс проверки, который следует хорошо известному принципу «что-то, что пользователь знает, и что-то, что у него есть».

Итак, пользователи предоставляют дополнительный «токен подтверждения» во время аутентификации — одноразовый код проверки пароля на основе алгоритма TOTP «Одноразовый пароль на основе времени».

2. Конфигурация Maven

Во-первых, чтобы использовать Google Authenticator в нашем приложении, нам необходимо:

  • Сгенерировать секретный ключ
  • Предоставить секретный ключ пользователю через QR-код
  • Проверьте токен, введенный пользователем с помощью этого секретного ключа.

Мы будем использовать простую серверную библиотеку для генерации/проверки одноразового пароля, добавив следующую зависимость в наш pom.xml :

<dependency>
<groupId>org.jboss.aerogear</groupId>
<artifactId>aerogear-otp-java</artifactId>
<version>1.0.0</version>
</dependency>

3. Объект пользователя

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

@Entity
public class User {
...
private boolean isUsing2FA;
private String secret;

public User() {
super();
this.secret = Base32.random();
...
}
}

Обратите внимание, что:

  • Мы сохраняем случайный секретный код для каждого пользователя, чтобы использовать его позже при создании проверочного кода.
  • Наша двухэтапная проверка не является обязательной

4. Дополнительный параметр входа

Во-первых, нам нужно будет настроить нашу конфигурацию безопасности, чтобы принять дополнительный параметр — токен проверки. Мы можем добиться этого, используя собственный AuthenticationDetailsSource :

Вот наш CustomWebAuthenticationDetailsSource :

@Component
public class CustomWebAuthenticationDetailsSource implements
AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new CustomWebAuthenticationDetails(context);
}
}

а вот CustomWebAuthenticationDetails :

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {

private String verificationCode;

public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
verificationCode = request.getParameter("code");
}

public String getVerificationCode() {
return verificationCode;
}
}

И наша конфигурация безопасности:

@Configuration
@EnableWebSecurity
public class LssSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomWebAuthenticationDetailsSource authenticationDetailsSource;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.authenticationDetailsSource(authenticationDetailsSource)
...
}
}

И, наконец, добавьте дополнительный параметр в нашу форму входа:

<labelth:text="#{label.form.login2fa}">
Google Authenticator Verification Code
</label>
<input type='text' name='code'/>

Примечание. Нам нужно установить наш собственный AuthenticationDetailsSource в нашей конфигурации безопасности.

5. Пользовательский поставщик аутентификации

Далее нам понадобится собственный AuthenticationProvider для обработки дополнительной проверки параметров:

public class CustomAuthenticationProvider extends DaoAuthenticationProvider {

@Autowired
private UserRepository userRepository;

@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
String verificationCode
= ((CustomWebAuthenticationDetails) auth.getDetails())
.getVerificationCode();
User user = userRepository.findByEmail(auth.getName());
if ((user == null)) {
throw new BadCredentialsException("Invalid username or password");
}
if (user.isUsing2FA()) {
Totp totp = new Totp(user.getSecret());
if (!isValidLong(verificationCode) || !totp.verify(verificationCode)) {
throw new BadCredentialsException("Invalid verfication code");
}
}

Authentication result = super.authenticate(auth);
return new UsernamePasswordAuthenticationToken(
user, result.getCredentials(), result.getAuthorities());
}

private boolean isValidLong(String code) {
try {
Long.parseLong(code);
} catch (NumberFormatException e) {
return false;
}
return true;
}

@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

Обратите внимание, что после того, как мы проверили код проверки одноразового пароля, мы просто делегировали аутентификацию нижестоящим.

Вот наш bean-компонент провайдера аутентификации

@Bean
public DaoAuthenticationProvider authProvider() {
CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}

6. Процесс регистрации

Теперь, чтобы пользователи могли использовать приложение для создания токенов, им необходимо правильно настроить все при регистрации.

Итак, нам нужно будет внести несколько простых изменений в процесс регистрации, чтобы позволить пользователям, выбравшим двухэтапную аутентификацию, сканировать QR-код, необходимый им для входа в систему позже .

Во-первых, мы добавляем этот простой ввод в нашу регистрационную форму:

Use Two step verification <input type="checkbox" name="using2FA" value="true"/>

Затем в нашем RegistrationController мы перенаправляем пользователей в зависимости от их выбора после подтверждения регистрации:

@GetMapping("/registrationConfirm")
public String confirmRegistration(@RequestParam("token") String token, ...) {
String result = userService.validateVerificationToken(token);
if(result.equals("valid")) {
User user = userService.getUser(token);
if (user.isUsing2FA()) {
model.addAttribute("qr", userService.generateQRUrl(user));
return "redirect:/qrcode.html?lang=" + locale.getLanguage();
}

model.addAttribute(
"message", messages.getMessage("message.accountVerified", null, locale));
return "redirect:/login?lang=" + locale.getLanguage();
}
...
}

А вот и наш метод generateQRUrl() :

public static String QR_PREFIX = 
"https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=";

@Override
public String generateQRUrl(User user) {
return QR_PREFIX + URLEncoder.encode(String.format(
"otpauth://totp/%s:%s?secret=%s&issuer=%s",
APP_NAME, user.getEmail(), user.getSecret(), APP_NAME),
"UTF-8");
}

А вот и наш qrcode.html :

<html>
<body>
<div id="qr">
<p>
Scan this Barcode using Google Authenticator app on your phone
to use it later in login
</p>
<img th:src="${param.qr[0]}"/>
</div>
<a href="/login" class="btn btn-primary">Go to login page</a>
</body>
</html>

Обратите внимание, что:

  • Метод generateQRUrl() используется для генерации URL-адреса QR-кода.
  • Этот QR-код будет сканироваться мобильными телефонами пользователей с помощью приложения Google Authenticator.
  • Приложение сгенерирует 6-значный код, действительный всего 30 секунд, который является желаемым проверочным кодом.
  • Этот код подтверждения будет проверен при входе в систему с использованием нашего пользовательского AuthenticationProvider.

7. Включите двухэтапную аутентификацию

Далее мы позаботимся о том, чтобы пользователи могли в любое время изменить свои настройки входа в систему, а именно:

@PostMapping("/user/update/2fa")
public GenericResponse modifyUser2FA(@RequestParam("use2FA") boolean use2FA)
throws UnsupportedEncodingException {
User user = userService.updateUser2FA(use2FA);
if (use2FA) {
return new GenericResponse(userService.generateQRUrl(user));
}
return null;
}

А вот updateUser2FA() :

@Override
public User updateUser2FA(boolean use2FA) {
Authentication curAuth = SecurityContextHolder.getContext().getAuthentication();
User currentUser = (User) curAuth.getPrincipal();
currentUser.setUsing2FA(use2FA);
currentUser = repository.save(currentUser);

Authentication auth = new UsernamePasswordAuthenticationToken(
currentUser, currentUser.getPassword(), curAuth.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
return currentUser;
}

А вот и фронтенд:

<div th:if="${#authentication.principal.using2FA}">
You are using Two-step authentication
<a href="#" onclick="disable2FA()">Disable 2FA</a>
</div>
<div th:if="${! #authentication.principal.using2FA}">
You are not using Two-step authentication
<a href="#" onclick="enable2FA()">Enable 2FA</a>
</div>
<br/>
<div id="qr" style="display:none;">
<p>Scan this Barcode using Google Authenticator app on your phone </p>
</div>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript">
function enable2FA(){
set2FA(true);
}
function disable2FA(){
set2FA(false);
}
function set2FA(use2FA){
$.post( "/user/update/2fa", { use2FA: use2FA } , function( data ) {
if(use2FA){
$("#qr").append('<img src="'+data.message+'" />').show();
}else{
window.location.reload();
}
});
}
</script>

8. Заключение

В этом кратком руководстве мы продемонстрировали, как реализовать двухфакторную аутентификацию с использованием Soft Token с Spring Security.

Полный исходный код, как всегда, можно найти на GitHub .