在前面的博文 Spring Boot2 实战系列之登录注册(二) - 登录实现 中实现了登录功能。这次继续完善常用的功能,就是在注册的时候可以向注册邮箱发送一个链接,打开该链接才能激活该账户。还有就是密码重置的功能。
pom 依赖如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.5.RELEASEversion>
<relativePath/>
parent>
<groupId>top.yekonglegroupId>
<artifactId>springboot-activate-account-sampleartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>springboot-activate-account-samplename>
<description>Activate account by email for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<passay.version>1.5.0passay.version>
<guava.version>29.0-jreguava.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
<dependency>
<groupId>org.passaygroupId>
<artifactId>passayartifactId>
<version>${passay.version}version>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>${guava.version}version>
dependency>
<dependency>
<groupId>com.h2databasegroupId>
<artifactId>h2artifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.properties
spring.mail.host=smtp.163.com
spring.mail.username=your_username
spring.mail.password=your_password
spring.mail.default-encoding=UTF-8
spring.mail.protocol=smtp
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.ssl.enable=true
support.email=your_username
# 国际化i18n配置,(包名.基础名)
spring.messages.basename=i18n.messages
spring.messages.encoding=UTF-8
# Thymeleaf 配置
# 禁止缓存
spring.thymeleaf.cache=false
这里主要写出改动或新增的类,其他的则和登录实现篇基本一致, 具体请查看仓库
PasswordResetToken.java, 密码重置校验 Token
package top.yekongle.activate.entity;
import java.util.Calendar;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
@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;
public PasswordResetToken() {
super();
}
public PasswordResetToken(final String token) {
super();
this.token = token;
this.expiryDate = calculateExpiryDate(EXPIRATION);
}
public PasswordResetToken(final String token, final User user) {
super();
this.token = token;
this.user = user;
this.expiryDate = calculateExpiryDate(EXPIRATION);
}
//
public Long getId() {
return id;
}
public String getToken() {
return token;
}
public void setToken(final String token) {
this.token = token;
}
public User getUser() {
return user;
}
public void setUser(final User user) {
this.user = user;
}
public Date getExpiryDate() {
return expiryDate;
}
public void setExpiryDate(final Date expiryDate) {
this.expiryDate = expiryDate;
}
private Date calculateExpiryDate(final int expiryTimeInMinutes) {
final Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(new Date().getTime());
cal.add(Calendar.MINUTE, expiryTimeInMinutes);
return new Date(cal.getTime().getTime());
}
public void updateToken(final String token) {
this.token = token;
this.expiryDate = calculateExpiryDate(EXPIRATION);
}
//
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((expiryDate == null) ? 0 : expiryDate.hashCode());
result = prime * result + ((token == null) ? 0 : token.hashCode());
result = prime * result + ((user == null) ? 0 : user.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PasswordResetToken other = (PasswordResetToken) obj;
if (expiryDate == null) {
if (other.expiryDate != null) {
return false;
}
} else if (!expiryDate.equals(other.expiryDate)) {
return false;
}
if (token == null) {
if (other.token != null) {
return false;
}
} else if (!token.equals(other.token)) {
return false;
}
if (user == null) {
if (other.user != null) {
return false;
}
} else if (!user.equals(other.user)) {
return false;
}
return true;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("Token [String=").append(token).append("]").append("[Expires").append(expiryDate).append("]");
return builder.toString();
}
}
UserController.java, 注册用户,激活账号,重置密码
package top.yekongle.activate.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.MessageSource;
import org.springframework.core.env.Environment;
import org.springframework.mail.MailAuthenticationException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.ModelAndView;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap;
import top.yekongle.activate.dto.PasswordDto;
import top.yekongle.activate.dto.UserDTO;
import top.yekongle.activate.entity.PasswordResetToken;
import top.yekongle.activate.entity.User;
import top.yekongle.activate.entity.VerificationToken;
import top.yekongle.activate.event.OnRegistrationCompleteEvent;
import top.yekongle.activate.exception.UserAlreadyExistException;
import top.yekongle.activate.service.UserService;
import top.yekongle.activate.util.GenericResponse;
import java.util.Calendar;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
/**
* @Author: Yekongle
* @Date: 2020年5月5日
*/
@Slf4j
@Controller
public class UserController {
@Autowired UserService userService;
@Autowired
private MessageSource messages;
@Autowired
LocaleResolver localeResolver;
@Autowired
ApplicationEventPublisher eventPublisher;
@Autowired
private JavaMailSender mailSender;
@Autowired
private Environment env;
@Autowired
private UserDetailsService userDetailsService;
// 注册页面
@GetMapping("/registration")
public String registration(Model model) {
model.addAttribute("formTitle", "注册");
return "registration";
}
// 用户注册
@PostMapping("/user/registration")
@ResponseBody
public GenericResponse registerUserAccount(@Valid UserDTO userDTO, HttpServletRequest request) {
User registered = userService.registerNewUserAccount(userDTO);
eventPublisher.publishEvent(new OnRegistrationCompleteEvent(registered, request.getLocale(), getAppUrl(request)));
return new GenericResponse("success");
}
// 激活用户账户
@GetMapping("/registrationConfirm.html")
public String confirmRegistration(HttpServletRequest request, HttpServletResponse response, RedirectAttributesModelMap model
, @RequestParam("token") String token) {
log.info("confirmRegistration");
Locale locale = localeResolver.resolveLocale(request);
log.info("token:{}" + token);
VerificationToken verificationToken = userService.getVerificationToken(token);
if (verificationToken == null) {
String message = messages.getMessage("auth.message.invalidToken", null, locale);
log.info("message:" + message);
model.addFlashAttribute("errMsg", message);
return "redirect:/badUser.html";
}
User user = verificationToken.getUser();
Calendar cal = Calendar.getInstance();
if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {
model.addFlashAttribute("message", messages.getMessage("auth.message.expired", null, locale));
model.addFlashAttribute("expired", true);
model.addFlashAttribute("token", token);
return "redirect:/badUser.html";
}
user.setEnabled(true);
userService.saveRegisteredUser(user);
model.addFlashAttribute("message", messages.getMessage("message.accountVerified", null, locale));
return "redirect:/login";
}
// 重新发送注册令牌
@GetMapping("/user/resendRegistrationToken")
@ResponseBody
public GenericResponse resendRegistrationToken(final HttpServletRequest request, final RedirectAttributesModelMap model, @RequestParam("token") final String existingToken) {
log.info("resendRegistrationToken");
final Locale locale = request.getLocale();
final VerificationToken newToken = userService.generateNewVerificationToken(existingToken);
final User user = userService.getUser(newToken.getToken());
final SimpleMailMessage email = constructResetVerificationTokenEmail(getAppUrl(request), request.getLocale(), newToken, user);
mailSender.send(email);
log.info("message: {}", messages.getMessage("message.resendToken", null, locale));
return new GenericResponse(messages.getMessage("message.resendToken", null, locale));
}
// 重置密码
@PostMapping("/user/resetPassword")
@ResponseBody
public GenericResponse resetPassword(final HttpServletRequest request, final RedirectAttributesModelMap model
, @RequestParam("email") final String userEmail) {
log.info("resetPassword: {}", userEmail);
final User user = userService.findUserByEmail(userEmail);
if (user == null) {
return new GenericResponse(messages.getMessage("message.userNotFound", null, request.getLocale()));
}
final String token = UUID.randomUUID().toString();
userService.createPasswordResetTokenForUser(user, token);
final SimpleMailMessage email = constructResetTokenEmail(getAppUrl(request), request.getLocale(), token, user);
mailSender.send(email);
return new GenericResponse(messages.getMessage("message.resetPasswordEmail", null, request.getLocale()));
}
// 更换密码页面
@GetMapping("/user/changePassword")
public String showChangePassword(final HttpServletRequest request, final RedirectAttributesModelMap model, @RequestParam("id") final long id, @RequestParam("token") final String token) {
log.info("showChangePassword, id:{}, token:{}", id, token);
final Locale locale = request.getLocale();
String result = userService.validatePasswordResetToken(token);
log.info("result:{}", result);
if(result != null) {
String message = messages.getMessage("auth.message." + result, null, locale);
log.info("message:{}", message);
model.addFlashAttribute("errMsg", message);
return "redirect:/login.html?";
} else {
model.addFlashAttribute("token", token);
return "redirect:/updatePassword.html";
}
}
@PostMapping("/user/savePassword")
@ResponseBody
public GenericResponse savePassword(final Locale locale, @Valid PasswordDto passwordDto) {
log.info("savePassword");
String result = userService.validatePasswordResetToken(passwordDto.getToken());
if(result != null) {
return new GenericResponse(messages.getMessage(
"auth.message." + result, null, locale));
}
Optional<User> user = userService.getUserByPasswordResetToken(passwordDto.getToken());
if(user.isPresent()) {
userService.changeUserPassword(user.get(), passwordDto.getNewPassword());
String message = messages.getMessage(
"message.resetPasswordSuc", null, locale);
log.info("message:{}", message);
return new GenericResponse(message);
} else {
return new GenericResponse(messages.getMessage(
"auth.message.invalid", null, locale));
}
}
private String getAppUrl(HttpServletRequest request) {
String appUrl = "http://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
return appUrl;
}
private final SimpleMailMessage constructResetVerificationTokenEmail(final String contextPath, final Locale locale
, final VerificationToken newToken, final User user) {
final String confirmationUrl = contextPath + "/registrationConfirm.html?token=" + newToken.getToken();
log.info("Url: {}", confirmationUrl);
final String message = messages.getMessage("message.resendToken", null, locale);
final SimpleMailMessage email = new SimpleMailMessage();
email.setSubject("Resend Registration Token");
email.setText(message + " \r\n" + confirmationUrl);
email.setTo(user.getEmail());
email.setFrom(env.getProperty("support.email"));
log.info("support.email:{}", env.getProperty("support.email"));
return email;
}
private final SimpleMailMessage constructResetTokenEmail(final String contextPath, final Locale locale, final String token, final User user) {
final String url = contextPath + "/user/changePassword?id=" + user.getId() + "&token=" + token;
log.info("url:{}", url);
final String message = messages.getMessage("message.resetPassword", null, locale);
final SimpleMailMessage email = new SimpleMailMessage();
email.setTo(user.getEmail());
email.setSubject("Reset Password");
email.setText(message + " \r\n" + url);
email.setFrom(env.getProperty("support.email"));
return email;
}
}
UserServiceImpl.java, 用户账号操作业务类
package top.yekongle.activate.service.impl;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Optional;
import java.util.UUID;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import top.yekongle.activate.dto.UserDTO;
import top.yekongle.activate.entity.PasswordResetToken;
import top.yekongle.activate.entity.User;
import top.yekongle.activate.entity.UserAuthority;
import top.yekongle.activate.entity.VerificationToken;
import top.yekongle.activate.exception.UserAlreadyExistException;
import top.yekongle.activate.repository.PasswordResetTokenRepository;
import top.yekongle.activate.repository.UserAuthorityRepository;
import top.yekongle.activate.repository.UserRepository;
import top.yekongle.activate.repository.VerificationTokenRepository;
import top.yekongle.activate.service.UserService;
/**
* @Description:
* @Author: Yekongle
* @Date: 2020年5月5日
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserAuthorityRepository userAuthorityRepository;
@Autowired
private VerificationTokenRepository tokenRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private PasswordResetTokenRepository passwordTokenRepository;
@Override
public User registerNewUserAccount(UserDTO userDTO) throws UserAlreadyExistException {
if (emailExists(userDTO.getEmail())) {
throw new UserAlreadyExistException("该邮箱已被注册:" + userDTO.getEmail());
}
log.info("UserDTO:" + userDTO.toString());
User user = new User();
user.setEmail(userDTO.getEmail());
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
userRepository.save(user);
UserAuthority userAuthority = new UserAuthority();
userAuthority.setUsername(userDTO.getEmail());
userAuthority.setRole("ROLE_USER");
userAuthorityRepository.save(userAuthority);
return user;
}
@Override
public VerificationToken getVerificationToken(String verificationToken) {
return tokenRepository.findByToken(verificationToken);
}
@Override
public VerificationToken generateNewVerificationToken(String token) {
VerificationToken vToken = tokenRepository.findByToken(token);
vToken.updateToken(UUID.randomUUID()
.toString());
vToken = tokenRepository.save(vToken);
return vToken;
}
@Override
public void saveRegisteredUser(User user) {
userRepository.save(user);
}
@Override
public void createVerificationToken(User user, String token) {
VerificationToken myToken = new VerificationToken(token, user);
tokenRepository.save(myToken);
}
@Override
public User getUser(String verificationToken) {
User user = tokenRepository.findByToken(verificationToken).getUser();
return user;
}
@Override
public PasswordResetToken getPasswordResetToken(String token) {
return passwordTokenRepository.findByToken(token);
}
@Override
public User findUserByEmail(String email) {
return userRepository.findByEmail(email);
}
@Override
public void createPasswordResetTokenForUser(User user, String token) {
final PasswordResetToken myToken = new PasswordResetToken(token, user);
passwordTokenRepository.save(myToken);
}
public String validatePasswordResetToken(String token) {
final PasswordResetToken passToken = passwordTokenRepository.findByToken(token);
return !isTokenFound(passToken) ? "invalidToken"
: isTokenExpired(passToken) ? "expired"
: null;
}
@Override
public void changeUserPassword(User user, String password) {
user.setPassword(passwordEncoder.encode(password));
userRepository.save(user);
}
@Override
public Optional<User> getUserByPasswordResetToken(String token) {
return Optional.ofNullable(passwordTokenRepository.findByToken(token).getUser());
}
private boolean emailExists(String email) {
return userRepository.findByEmail(email) != null;
}
private boolean isTokenFound(PasswordResetToken passToken) {
return passToken != null;
}
private boolean isTokenExpired(PasswordResetToken passToken) {
final Calendar cal = Calendar.getInstance();
return passToken.getExpiryDate().before(cal.getTime());
}
}
OnRegistrationCompleteEvent.java, 注册监听事件
package top.yekongle.activate.event;
import java.util.Locale;
import lombok.*;
import org.springframework.context.ApplicationEvent;
import top.yekongle.activate.entity.User;
/**
* @Description:
* @Author: Yekongle
* @Date: 2020年6月6日
*/
@Getter
@Setter
public class OnRegistrationCompleteEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private String appUrl;
private Locale locale;
private User user;
public OnRegistrationCompleteEvent(User user, Locale locale, String appUrl) {
super(user);
this.user = user;
this.locale = locale;
this.appUrl = appUrl;
}
}
RegistrationListener.java, 注册事件监听, 发送激活邮件到用户邮箱
package top.yekongle.activate.listener;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.context.MessageSource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import top.yekongle.activate.entity.User;
import top.yekongle.activate.event.OnRegistrationCompleteEvent;
import top.yekongle.activate.service.UserService;;
/**
* @Description:
* @Author: Yekongle
* @Date: 2020年6月6日
*/
@Slf4j
@Component
public class RegistrationListener implements ApplicationListener<OnRegistrationCompleteEvent> {
@Autowired
private UserService service;
@Autowired
private MessageSource messages;
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String emailFrom;
@Override
public void onApplicationEvent(OnRegistrationCompleteEvent event) {
this.confirmRegistration(event);
}
private void confirmRegistration(OnRegistrationCompleteEvent event) {
User user = event.getUser();
String token = UUID.randomUUID().toString();
service.createVerificationToken(user, token);
String recipientAddress = user.getEmail();
String subject = "Registration Confirmation";
String confirmationUrl = event.getAppUrl() + "/registrationConfirm.html?token=" + token;
log.info("confirmationUrl: {}" + confirmationUrl);
String message = messages.getMessage("message.regSucc", null, event.getLocale());
log.info("recipientAddress: {}", user.getEmail());
SimpleMailMessage email = new SimpleMailMessage();
email.setFrom(emailFrom);
email.setTo(recipientAddress);
email.setSubject(subject);
email.setText(message + "\r\n" + confirmationUrl);
mailSender.send(email);
}
}
启动项目
项目已上传至 Github: https://github.com/yekongle/springboot-code-samples/tree/master/springboot-activate-account-sample , 希望对小伙伴们有帮助哦。
参考链接: