Двухфакторная аутентификация с Spring Security
1. обзор
В этом руководстве мы собираемся реализоватьTwo Factor Authentication functionality с помощью Soft Token и Spring Security.
Мы собираемся добавить новую функциональность вan existing, simple login flow и использоватьGoogle Authenticator app для генерации токенов.
Проще говоря, двухфакторная аутентификация - это процесс проверки, который следует общеизвестному принципу «что-то, что пользователь знает, и что-то, что есть у пользователя».
Таким образом, пользователи предоставляют дополнительный «токен подтверждения» во время аутентификации - код подтверждения одноразового пароля, основанный на алгоритме временного одноразового пароляTOTP.
2. Конфигурация Maven
Во-первых, чтобы использовать Google Authenticator в нашем приложении, нам необходимо:
-
Генерация секретного ключа
-
Предоставить секретный ключ пользователю через QR-код
-
Проверьте токен, введенный пользователем, используя этот секретный ключ.
Мы будем использовать простой серверныйlibrary для генерации / проверки одноразового пароля, добавив следующую зависимость к нашемуpom.xml:
org.jboss.aerogear
aerogear-otp-java
1.0.0
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 {
@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)
...
}
}
И, наконец, добавьте дополнительный параметр в нашу форму входа:
Google Authenticator Verification 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
public DaoAuthenticationProvider authProvider() {
CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
6. Процесс регистрации
Теперь, чтобы пользователи могли использовать приложение для генерации токенов, им нужно будет правильно настроить все при регистрации.
Итак, нам нужно будет внести несколько простых изменений в процесс регистрации, чтобы разрешить пользователям, выбравшим двухэтапную аутентификацию,scan the QR-code they need to login later.
Сначала мы добавим этот простой ввод в нашу регистрационную форму:
Use Two step verification
Затем в нашемRegistrationController - мы перенаправляем пользователей в зависимости от их выбора после подтверждения регистрации:
@RequestMapping(value = "/registrationConfirm", method = RequestMethod.GET)
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:
Scan this Barcode using Google Authenticator app on your phone
to use it later in login
Go to login page
Обратите внимание, что:
-
МетодgenerateQRUrl() используется для генерации URL-адреса QR-кода
-
Этот QR-код будет отсканирован пользователями мобильных телефонов с помощью приложения Google Authenticator.
-
Приложение сгенерирует 6-значный код, который действителен только в течение 30 секунд, который является желаемым кодом подтверждения
-
Этот код подтверждения будет проверен при входе в систему с использованием нашего пользовательскогоAuthenticationProvider
7. Включить двухэтапную аутентификацию
Далее мы позаботимся о том, чтобы пользователи могли изменить свои настройки входа в систему в любое время:
@RequestMapping(value = "/user/update/2fa", method = RequestMethod.POST)
@ResponseBody
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;
}
А вот и внешний интерфейс:
You are using Two-step authentication
Disable 2FA
You are not using Two-step authentication
Enable 2FA
8. Заключение
В этом кратком руководстве мы продемонстрировали, как реализовать реализацию двухфакторной аутентификации с использованием Soft Token и Spring Security.
Полный исходный код можно найти - как всегда -over on GitHub.