Spring Security - сбросьте свой пароль
1. обзор
В этом руководстве - мы продолжаем текущийRegistration with Spring Security series, рассматриваяthe basic “I forgot my password” feature - чтобы пользователь мог безопасно сбросить свой собственный пароль, когда ему нужно.
2. Токен сброса пароля
Давайте начнем с создания объектаPasswordResetToken, чтобы использовать его для сброса пароля пользователя:
@Entity
public class PasswordResetToken {
private static final int EXPIRATION = 60 * 24;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String token;
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;
private Date expiryDate;
}
При срабатывании сброса пароля - будет создан токен иa special link containing this token will be emailed to the user.
Токен и ссылка будут действительны только в течение определенного периода времени (в данном примере 24 часа).
3. forgotPassword.htmlс
Первая страница в процессе -the “I forgot my password” page, на которой пользователю предлагается ввести адрес электронной почты, чтобы начать фактический процесс сброса.
Итак, давайте создадим простойforgotPassword.html, запрашивающий у пользователя адрес электронной почты:
reset
registration
login
Теперь нам нужно связать эту новую страницу «reset password» со страницы входа:
4. СоздайтеPasswordResetToken
Начнем с создания новогоPasswordResetToken и отправим его пользователю по электронной почте:
@RequestMapping(value = "/user/resetPassword",
method = RequestMethod.POST)
@ResponseBody
public GenericResponse resetPassword(HttpServletRequest request,
@RequestParam("email") String userEmail) {
User user = userService.findUserByEmail(userEmail);
if (user == null) {
throw new UserNotFoundException();
}
String token = UUID.randomUUID().toString();
userService.createPasswordResetTokenForUser(user, token);
mailSender.send(constructResetTokenEmail(getAppUrl(request),
request.getLocale(), token, user));
return new GenericResponse(
messages.getMessage("message.resetPasswordEmail", null,
request.getLocale()));
}
А вот методcreatePasswordResetTokenForUser():
public void createPasswordResetTokenForUser(User user, String token) {
PasswordResetToken myToken = new PasswordResetToken(token, user);
passwordTokenRepository.save(myToken);
}
А вот методconstructResetTokenEmail() - используется для отправки электронного письма с токеном сброса:
private SimpleMailMessage constructResetTokenEmail(
String contextPath, Locale locale, String token, User user) {
String url = contextPath + "/user/changePassword?id=" +
user.getId() + "&token=" + token;
String message = messages.getMessage("message.resetPassword",
null, locale);
return constructEmail("Reset Password", message + " \r\n" + url, user);
}
private SimpleMailMessage constructEmail(String subject, String body,
User user) {
SimpleMailMessage email = new SimpleMailMessage();
email.setSubject(subject);
email.setText(body);
email.setTo(user.getEmail());
email.setFrom(env.getProperty("support.email"));
return email;
}
Обратите внимание, как мы использовали простой объектGenericResponse для представления нашего ответа клиенту:
public class GenericResponse {
private String message;
private String error;
public GenericResponse(String message) {
super();
this.message = message;
}
public GenericResponse(String message, String error) {
super();
this.message = message;
this.error = error;
}
}
5. ОбработатьPasswordResetToken
Пользователь получает электронное письмо с уникальной ссылкой для сброса пароля и щелкает ссылку:
@RequestMapping(value = "/user/changePassword", method = RequestMethod.GET)
public String showChangePasswordPage(Locale locale, Model model,
@RequestParam("id") long id, @RequestParam("token") String token) {
String result = securityService.validatePasswordResetToken(id, token);
if (result != null) {
model.addAttribute("message",
messages.getMessage("auth.message." + result, null, locale));
return "redirect:/login?lang=" + locale.getLanguage();
}
return "redirect:/updatePassword.html?lang=" + locale.getLanguage();
}
А вот методvalidatePasswordResetToken():
public String validatePasswordResetToken(long id, String token) {
PasswordResetToken passToken =
passwordTokenRepository.findByToken(token);
if ((passToken == null) || (passToken.getUser()
.getId() != id)) {
return "invalidToken";
}
Calendar cal = Calendar.getInstance();
if ((passToken.getExpiryDate()
.getTime() - cal.getTime()
.getTime()) <= 0) {
return "expired";
}
User user = passToken.getUser();
Authentication auth = new UsernamePasswordAuthenticationToken(
user, null, Arrays.asList(
new SimpleGrantedAuthority("CHANGE_PASSWORD_PRIVILEGE")));
SecurityContextHolder.getContext().setAuthentication(auth);
return null;
}
Как видите, если токен действителен, пользователь получит право изменить свой пароль, предоставив емуCHANGE_PASSWORD_PRIVILEGE, и направить его на страницу для обновления пароля.
Интересно отметить, что эта новая привилегия может использоваться только для изменения пароля (как следует из названия), и поэтому предоставление ее программно пользователю безопасно.
6. Измени пароль
В этот момент пользователь видит простую страницуPassword Reset, где единственно возможный вариант -provide a new password:
6.1. updatePassword.htmlс
reset
6.2. Сохранить пароль пользователя
Наконец, когда отправляется предыдущий пост-запрос, новый пароль пользователя сохраняется:
@RequestMapping(value = "/user/savePassword", method = RequestMethod.POST)
@ResponseBody
public GenericResponse savePassword(Locale locale,
@Valid PasswordDto passwordDto) {
User user =
(User) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
userService.changeUserPassword(user, passwordDto.getNewPassword());
return new GenericResponse(
messages.getMessage("message.resetPasswordSuc", null, locale));
}
А вот методchangeUserPassword():
public void changeUserPassword(User user, String password) {
user.setPassword(passwordEncoder.encode(password));
repository.save(user);
}
Обратите внимание, что мы защищаем запросы на обновление и сохраняем пароли - следующим образом:
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/updatePassword*",
"/user/savePassword*",
"/updatePassword*")
.hasAuthority("CHANGE_PASSWORD_PRIVILEGE")
...
7. Заключение
В этой статье мы реализовали простую, но очень полезную функцию для зрелого процесса аутентификации - возможность сброса собственного пароля в качестве пользователя системы.
full implementation этого руководства можно найти вthe GitHub project - это проект на основе Eclipse, поэтому его должно быть легко импортировать и запускать как есть.