[译] 学习 Spring Security(四):使用邮箱激活新账户
www.baeldung.com/registratio…
作者:Eugen Paraschiv
译者:oopsguy.com
1、概述
本文续之前的 Spring Security 系列之注册流程中缺失的部分 — 验证用户的电子邮件以确认帐户。
注册确认机制强制用户在成功注册后执行确认注册电子邮件中的操作,以验证其电子邮件地址并激活帐户。用户通过单击电子邮件中的唯一激活链接来完成激活操作。
根据此逻辑,新注册的用户无法登录到系统,除非完成了该流程。
2、验证 Token
我们将使用一个简单的验证令牌作为验证用户的凭据。
2.1、VerificationToken 实体
VerificationToken 实体必须符合以下标准:
- 它必须指向 User(通过一个单向关系)
- 它将在注册后立即创建
- 它将在创建后 24 小时内过期
- 有一个唯一、随机生成的值
第 2 和 3 点是注册逻辑的一部分。其余的两个实现位于简单的 VerificationToken 实体中,如示例 2.1。
示例 2.1
@Entity
public class VerificationToken {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;private Date calculateExpiryDate(int expiryTimeInMinutes) {Calendar cal = Calendar.getInstance();cal.setTime(new Timestamp(cal.getTime().getTime()));cal.add(Calendar.MINUTE, expiryTimeInMinutes);return new Date(cal.getTime().getTime());}// standard constructors, getters and setters
}
复制代码
注意 User 上的 nullable = false,确保了 VerificationToken <-> User 关联中数据的完整性和一致性。
2.2、将 enabled 字段添加到 User 中
当用户注册时,此 enabled 字段将被设置为 false。在帐户验证过程中,如果通过,则置为 true。
添加字段到 User 实体中:
public class User {...@Column(name = "enabled")private boolean enabled;public User() {super();this.enabled=false;}...
}
复制代码
请注意,我们也将该字段的默认值设置为 false。
3、帐户注册期间
让我们添加两个额外的业务逻辑到用户注册用例中:
- 为 User 生成 VerificationToken 并保存
- 发送电子邮件进行帐户确认 — 其中包含带有 VerificationToken 值的确认链接
3.1、使用 Spring Event 创建令牌并发送验证邮件
这两个额外的逻辑不应该由控制器直接执行,因为它们是并行的后台任务。
控制器将发布一个 Spring ApplicationEvent 来触发这些任务的执行。这和注入 ApplicationEventPublisher 使用它来发布注册一样简单。
示例 3.1 展示了这个简单的逻辑:
示例 3.1
@Autowired
ApplicationEventPublisher eventPublisher@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount(@ModelAttribute("user") @Valid UserDto accountDto, BindingResult result, WebRequest request, Errors errors) {if (result.hasErrors()) {return new ModelAndView("registration", "user", accountDto);}User registered = createUserAccount(accountDto);if (registered == null) {result.rejectValue("email", "message.regError");}try {String appUrl = request.getContextPath();eventPublisher.publishEvent(new OnRegistrationCompleteEvent(registered, request.getLocale(), appUrl));} catch (Exception me) {return new ModelAndView("emailError", "user", accountDto);}return new ModelAndView("successRegister", "user", accountDto);
}
复制代码
另外需要注意的是包围事件发布的 try catch 块。这段代码代表了只要在发布事件后执行的逻辑中存在异常就展示一个错误页面。此处的逻辑就是发送电子邮件。
3.2、Event 与 Listener
现在让我们看看控制器发出的这个新的 OnRegistrationCompleteEvent 的实际实现,以及要处理它的监听器:
例 3.2.1 — OnRegistrationCompleteEvent
public class OnRegistrationCompleteEvent extends ApplicationEvent {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;}// standard getters and setters
}
复制代码
例 3.2.2 — RegistrationListener 处理 OnRegistrationCompleteEvent
@Component
public class RegistrationListener implementsApplicationListener<OnRegistrationCompleteEvent> {@Autowiredprivate IUserService service;@Autowiredprivate MessageSource messages;@Autowiredprivate JavaMailSender mailSender;@Overridepublic 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() + "/regitrationConfirm.html?token=" + token;String message = messages.getMessage("message.regSucc", null, event.getLocale());SimpleMailMessage email = new SimpleMailMessage();email.setTo(recipientAddress);email.setSubject(subject);email.setText(message + " rn" + "http://localhost:8080" + confirmationUrl);mailSender.send(email);}
}
复制代码
在此处,confirmRegistration 方法将接收 OnRegistrationCompleteEvent,从中提取所有必要的 User 信息,创建验证令牌,将其保存,然后在确认注册链接中将其作为参数发送。
如上所述,JavaMailSender 引发的任何 javax.mail.AuthenticationFailedException 都将由控制器处理。
3.3、处理验证令牌参数
当用户收到确认注册链接时点击它。
一旦点击,控制器将提取 GET 请求中的令牌参数的值,并将使用它来启用 User。
我们来示例 3.3.1 中的这个流程:
例 3.3.1 — RegistrationController 处理注册确认
@Autowired
private IUserService service;@RequestMapping(value = "/regitrationConfirm", method = RequestMethod.GET)
public String confirmRegistration(WebRequest request, Model model, @RequestParam("token") String token) {Locale locale = request.getLocale();VerificationToken verificationToken = service.getVerificationToken(token);if (verificationToken == null) {String message = messages.getMessage("auth.message.invalidToken", null, locale);model.addAttribute("message", message);return "redirect:/badUser.html?lang=" + locale.getLanguage();}User user = verificationToken.getUser();Calendar cal = Calendar.getInstance();if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {String messageValue = messages.getMessage("auth.message.expired", null, locale)model.addAttribute("message", messageValue);return "redirect:/badUser.html?lang=" + locale.getLanguage();} user.setEnabled(true); service.saveRegisteredUser(user); return "redirect:/login.html?lang=" + request.getLocale().getLanguage();
}
复制代码
如果出现以下情况,用户将被重定向到错误页面并显示相应的信息:
- 由于某种原因 VerificationToken 不存在
- VerificationToken 已过期
见示例 3.3.2 的错误页面。
例 3.3.2 — badUser.html
<html>
<body><h1 th:text="${param.message[0]}">Error Message</h1><a th:href="@{/registration.html}"th:text="#{label.form.loginSignUp}">signup</a>
</body>
</html>
复制代码
如果没有发现错误,则启用用户。
在处理 VerificationToken 检查和过期流程中有两个地方可以改进:
- 我们可以使用 Cron 作业在后台检查令牌是否过期
- 一旦过期,我们可以让用户有机会获得新的令牌
我们将生成新令牌流程推迟到后面的文章再讲,现在假设用户确实在这里成功验证了令牌。
4、将帐户激活检查添加到登录流程
我们需要添加检查用户是否启用的代码:
我们来看示例 4.1。其展示了 MyUserDetailsService 的 loadUserByUsername方法。
例 4.1
@Autowired
UserRepository userRepository;public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {boolean enabled = true;boolean accountNonExpired = true;boolean credentialsNonExpired = true;boolean accountNonLocked = true;try {User user = userRepository.findByEmail(email);if (user == null) {throw new UsernameNotFoundException("No user found with username: " + email);}return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword().toLowerCase(), user.isEnabled(), accountNonExpired, credentialsNonExpired, accountNonLocked, getAuthorities(user.getRole()));} catch (Exception e) {throw new RuntimeException(e);}
}
复制代码
正如我们所看到的,现在 MyUserDetailsService 不使用 User 的 enabled 标志。
现在,我们将添加一个 AuthenticationFailureHandler 来自定义来自 MyUserDetailsService 的异常消息。我们的 CustomAuthenticationFailureHandler 如示例 4.2 所示:
例 4.2 — CustomAuthenticationFailureHandler:
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {@Autowiredprivate MessageSource messages;@Autowiredprivate LocaleResolver localeResolver;@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)throws IOException, ServletException {setDefaultFailureUrl("/login.html?error=true");super.onAuthenticationFailure(request, response, exception);Locale locale = localeResolver.resolveLocale(request);String errorMessage = messages.getMessage("message.badCredentials", null, locale);if (exception.getMessage().equalsIgnoreCase("User is disabled")) {errorMessage = messages.getMessage("auth.message.disabled", null, locale);} else if (exception.getMessage().equalsIgnoreCase("User account has expired")) {errorMessage = messages.getMessage("auth.message.expired", null, locale);}request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, errorMessage);}
}
复制代码
我们需要修改 login.html 以显示错误消息。
示例 4.3 — 在 login.html 处显示错误消息:
<div th:if="${param.error != null}"th:text="${session[SPRING_SECURITY_LAST_EXCEPTION]}">error</div>
复制代码
5、适配持久层
现在让我们来看看一些涉及到验证令牌和用户操作的实际实现。
将涵盖以下内容:
- 一个新的 VerificationTokenRepository
- IUserInterface 中的新方法及其对新 CRUD 操作的实现需求
示例 5.1 — 5.3 展示新的接口和实现:
示例 5.1 — VerificationTokenRepository
public interface VerificationTokenRepository extends JpaRepository<VerificationToken, Long> {VerificationToken findByToken(String token);VerificationToken findByUser(User user);
}
复制代码
示例5.2 — IUserService 接口
public interface IUserService {User registerNewUserAccount(UserDto accountDto) throws EmailExistsException;User getUser(String verificationToken);void saveRegisteredUser(User user);void createVerificationToken(User user, String token);VerificationToken getVerificationToken(String VerificationToken);
}
复制代码
示例 5.3 — UserService
@Service
@Transactional
public class UserService implements IUserService {@Autowiredprivate UserRepository repository;@Autowiredprivate VerificationTokenRepository tokenRepository;@Overridepublic User registerNewUserAccount(UserDto accountDto) throws EmailExistsException {if (emailExist(accountDto.getEmail())) {throw new EmailExistsException("There is an account with that email adress: "+ accountDto.getEmail());}User user = new User();user.setFirstName(accountDto.getFirstName());user.setLastName(accountDto.getLastName());user.setPassword(accountDto.getPassword());user.setEmail(accountDto.getEmail());user.setRole(new Role(Integer.valueOf(1), user));return repository.save(user);}private boolean emailExist(String email) {User user = repository.findByEmail(email);if (user != null) {return true;}return false;}@Overridepublic User getUser(String verificationToken) {User user = tokenRepository.findByToken(verificationToken).getUser();return user;}@Overridepublic VerificationToken getVerificationToken(String VerificationToken) {return tokenRepository.findByToken(VerificationToken);}@Overridepublic void saveRegisteredUser(User user) {repository.save(user);}@Overridepublic void createVerificationToken(User user, String token) {VerificationToken myToken = new VerificationToken(token, user);tokenRepository.save(myToken);}
}
复制代码
6、总结
在本文中,我们已经介绍了注册流程,包括一个基于电子邮件的帐户激活流程。
帐户激活逻辑需要通过电子邮件向用户发送验证令牌,以便他们可以将信息发送回控制器以验证身份。
这个注册与 Spring Security 教程的实现可以在 GitHub 项目中找到 — 这是一个基于 Eclipse 的项目,因此应该很容易导入运行。
原文项目源码
- github.com/eugenp/spri…
[译] 学习 Spring Security(四):使用邮箱激活新账户相关推荐
- Spring Security(四) —— 核心过滤器源码分析
摘要: 原创出处 https://www.cnkirito.moe/spring-security-4/ 「老徐」欢迎转载,保留摘要,谢谢! 4 过滤器详解 前面的部分,我们关注了Spring Sec ...
- Spring Security(四) —— RememberMe
一:简介 Remember Me 即记住我,常用于 Web 应用的登录页目的是让用户选择是否记住用户的登录状态.当用户选择了 Remember Me 选项,则在有效期内若用户重新访问同一个 Web 应 ...
- 【Spring】12、Spring Security 四种使用方式
spring security使用分类: 如何使用spring security,相信百度过的都知道,总共有四种用法,从简到深为:1.不用数据库,全部数据写在配置文件,这个也是官方文档里面的demo: ...
- 学习Spring Security
https://www.jianshu.com/p/4fe340b56fe4 Spring Security 系列 15 篇
- 1.spring security简单的demo-适合萌新
文章目录 1.spring security入门 1.创建项目,引入依赖 2.security的其他配置方式 2.2基于内存 2.3HttpSecurity(入门配置此文件) 2.4多个HttpSec ...
- 学习Spring(四) -- Spring的继承与依赖
2019独角兽企业重金招聘Python工程师标准>>> 继承 在spring配置文件中,在bean元素里可以使用parent属性来实现配置信息的继承,比如需要十个学生类的bean,其 ...
- [译]Spring Session 与 Spring Security
原文:http://docs.spring.io/spring-session/docs/current-SNAPSHOT/reference/html5/guides/security.html 本 ...
- 循序渐进学spring security 第八篇,如何配置密码加密?是否支持多种加密方案?
文章目录 回顾 密码明文会带来什么问题? 如何加密? PasswordEncoder 加密接口 如何配置? 加密的密码在登录的时候是怎么校验的? 默认的加密是什么? DaoAuthentication ...
- Java案例-用户注册邮箱验证将邮箱激活码存入redis功能实现
<–start–> 用户注册时,要求用户填写注册邮箱,然后后台向用户的注册邮箱中发送绑定邮件,并将激活码保存到redis中. 手机短信验证码因为时效期短,只需要将短信验证码存储到sessi ...
- Spring Security(一):最简单的Spring Security程序
一:简介 权限管理常用的有Apache Shiro和Spring Security, Apache Shiro简单易用,Spring Security集成复杂,但功能强大,可以与Spring的其它框架 ...
最新文章
- linux执行命令段错误,Linux运行fortran程序 出现段错误(segmentation fault)
- 使用面向 iOS 的本机插件扩展 PhoneGap
- 【Git】Git 本地的撤销修改和删除操作
- 石大ACM2587解题报告
- web数据库连接注意事项
- spring Boot打可执行的jar包
- nginx重新安装 引起的问题
- Mysql快照读和当前读
- 计算机检测维修与数据恢复课件,2017年全国职业院校技能大赛中职组“计算机检测维修与数据恢复”赛项说明会ppt课件.ppt...
- 30 张图解 | 面试官问我高并发服务模型哪家强?
- 基于java的九宫格求解程序。以荷兰数学家设计的世界最难九宫格为例。
- c语言赌徒游戏,The Gambler(I)赌徒(英文版)
- 双屏扩展桌面类软件,展示界面位置不正确,或相反,如何解决?
- 两个队列实现一个栈(C++实现)
- 概率论与数理统计学习笔记(3)——Pearson相关系数与Spearman相关系数
- 精进——如何成为很厉害的人(采铜)
- 腾讯要放弃 TIM 了?
- fedora 20 安裝 及 配置桌面環境
- 二叉树练习:最小函数值
- Python nonlocal