Springboot系列之接入邮箱验证码
...大约 10 分钟
Springboot-邮箱验证码
构建说明
构建思路
- 基于spring-boot-starter-email 工具包(或基于原生javax.mail)实现邮件发送功能
- 借助junit-vintage-engine工具包或者freemarker模板实现html邮件模板功能
- 利用easy-captcha工具包生成随机验证码(或者自定义工具类生成随机验证码)
- 缓存借助guvcache或者redis缓存进行构建(对比guvcache和redis的优缺点)
构建步骤
1.准备一个邮件发送账号,开通SMTP服务
2.项目配置:引入邮箱服务所需依赖,构建功能实现
3.向目标邮箱发送随机验证码、用户输入验证码进行校验完成注册
实现说明
1.邮件服务构建(实现邮件发送)
1)SMTP/MAP服务开启
以QQ邮箱为例,开启SMTP/MAP服务并配置,进行授权码管理(生成授权码,可设置备注信息管理授权码),该授权码是用作邮件发送密码
2)构建email服务
引入相关依赖(spring-boot-starter-mail邮箱服务、freemarker模板)、配置邮箱服务参数
<!-- 邮箱配置相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!--引入模板引擎(动态模板构建美化页面显示) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
spring:
# 邮箱配置
mail:
# 负责发送验证码的邮箱配置
email: # 发送者邮箱
host: smtp.qq.com
port: 465
username: # 发送者邮箱
password: qehuasinhglyddhc # 授权密码(非邮箱密码)授权码用于登录第三方邮件客户端的专用密码
protocol: smtps
default-encoding: UTF-8 # 默认编码格式
properties:
mail:
debug: true #启动debug调试
smtp:
socketFactory:
class: javax.net.ssl.SSLSocketFactory #SSL连接配置
# 自定义项目配置相关
custom:
emailCode:
expiration: 30
构建email服务
/**
* 邮件服务接口定义
*/
public interface EmailService {
/**
* 发送邮件
* @param to
* @param subject
* @param content
*/
void sendMail(String to, String subject, String content);
}
/**
* 邮箱服务实现
*/
@Service
public class EmailServiceImpl implements EmailService {
@Resource
private JavaMailSender mailSender;
@Value("${spring.mail.email}")
private String email;
@Override
public void sendMail(String to, String subject, String content) {
// 读取邮箱配置
if (email == null) {
throw new RuntimeException("邮箱配置异常");
}
// 创建邮件消息
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = null;
try {
helper = new MimeMessageHelper(message, true);
// 设置发件人邮箱
helper.setFrom(email);
// 设置收件人信息
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
} catch (MessagingException e) {
throw new RuntimeException(e);
}
// 发送邮件
mailSender.send(message);
}
}
- 发送邮件模板(resources/templates/email-code.ftl)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style>
@page {
margin: 0;
}
</style>
</head>
<body>
<div class="header">
<div style="padding: 10px;padding-bottom: 0px;">
<p style="margin-bottom: 10px;padding-bottom: 0px;">尊敬的用户,您好:</p>
<p style="text-indent: 2em; margin-bottom: 10px;">您正在注册【一人の境】平台账号,您的验证码为:</p>
<p class="code-text">${code}</p>
<p style="text-indent: 2em; margin-bottom: 10px;">请确认查收!验证码有效期为${expiration}分钟,请在有效期内完成操作</p>
<div class="footer">
</div>
</div>
</div>
</body>
</html>
<style lang="css">
body {
margin: 0px;
padding: 0px;
font: 100% SimSun, Microsoft YaHei, Times New Roman, Verdana, Arial, Helvetica, sans-serif;
color: #000;
}
.header {
height: auto;
width: 820px;
min-width: 820px;
margin: 0 auto;
margin-top: 20px;
border: 1px solid #eee;
}
.code-text {
text-align: center;
font-family: Times New Roman;
font-size: 22px;
color: #C60024;
padding: 20px 0px;
margin-bottom: 10px;
font-weight: bold;
background: #ebebeb;
}
.footer {
margin: 0 auto;
z-index: 111;
width: 800px;
margin-top: 30px;
border-top: 1px solid #DA251D;
}
</style>
3)构建发送验证码接口
public interface AccountService {
/**
* 发送邮箱验证码
* @return
*/
boolean sendEmailCode(String email);
}
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private EmailService emailService;
@Value("${custom.emailCode.expiration}")
private Long expiration;
@Override
public void userLogout() {
// ShiroUtil.deleteCache();
// 退出登陆
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
// 销毁SESSION(清理权限缓存)
subject.logout();
}
}
@Override
public boolean sendEmailCode(String email) {
// todo 查看注册邮箱是否存在,引入缓存机制
// 获取发送邮箱验证码的HTML模板(resources/templates/下存放模板信息)
TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
Template template = engine.getTemplate("email-code.ftl");
// 调用邮箱服务发送验证码信息
String subject = "邮箱验证码";
String content = template.render(Dict.create().set("code", RandomUtil.randomNumbers(6)).set("expiration", expiration));
emailService.sendMail(email,subject,content);
// 返回响应信息
return true;
}
}
@RestController
@RequestMapping("/account")
public class AccountController {
@Resource
private AccountService accountService;
/**
* 发送邮箱验证码
* @return
*/
@GetMapping("/sendEmailCode")
public BaseResponse<Boolean> sendEmailCode(@RequestParam String email) {
// 调用验证码服务获取邮箱验证码信息
accountService.sendEmailCode(email);
return ResultUtils.success(true);
}
}
4)测试验证码发送
基于上述步骤,构建验证码发送接口,通过接口调试测试验证码发送,如果发送出现问题则依次排查配置等问题(例如发送者的邮箱、授权码等)
参考访问API:http://localhost:8101/api/account/sendEmailCode
2.引入缓存(嵌入业务逻辑)
接口定义
- 发送验证码接口:发送验证码到指定邮箱
- 换绑邮箱接口:邮箱换绑
缓存配置
引入redis缓存
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--fastjson依赖 Redis使用FastJson序列化-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
配置redis:(此处使用springboot-redis,区分BI模块redis限流引入的RedissonConfig、RedisLimiterManager)
- application.yml:配置redis
- RedisConfig:构建redis客户端
- RedisCache:构建redis缓存工具类
- FastJsonRedisSerializer:redis数据序列化器(使用fastjson进行序列化)
spring:
# Redis 配置
redis:
database: 1
host: localhost
port: 6379
timeout: 5000
password: 123456
@Configuration
public class RedisConfig {
@Value("${spring.redis.database}")
private Integer database;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private Integer port;
// 如果redis没有默认密码则不用写
@Value("${spring.redis.password}")
private String password;
// 显式定义redisConnectionFactory
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
// 设定redis配置(IP、端口、密码登配置)
LettuceConnectionFactory redisConnectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
redisConnectionFactory.setPassword(password);
return redisConnectionFactory;
}
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 删除Hash中的数据
*
* @param key
* @param hkey
*/
public void delCacheMapValue(final String key, final String hkey)
{
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
/**
* 更新redis文章浏览量
* @param key 哪一个哈希结构
* @param hKey 哈希表里哪一个数据
* @param v 更改值
*/
public void incrementCacheMapValue(String key,String hKey,long v){
redisTemplate.boundHashOps(key).increment(hKey, v);
}
}
/**
* Redis使用FastJson序列化
*/
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}
接口实现
controller:定义接口(发送邮箱验证码)、换绑邮箱
@RestController
@RequestMapping("/account")
public class AccountController {
@Resource
private AccountService accountService;
/**
* 发送邮箱验证码
* @return
*/
@GetMapping("/sendEmailCode")
public BaseResponse<Boolean> sendEmailCode(@RequestParam String email) {
// 调用验证码服务获取邮箱验证码信息
boolean res = accountService.sendEmailCode(email);
return ResultUtils.success(res);
}
/**
* 换绑邮箱
* @return
*/
@GetMapping("/bindEmail")
public BaseResponse<Boolean> bindEmail(@RequestParam String email,@RequestParam String code) {
// 调用验证码服务获取邮箱验证码信息
boolean res = accountService.bindEmail(email,code);
return ResultUtils.success(res);
}
}
service
public interface AccountService {
/**
* 发送邮箱验证码
* @return
*/
boolean sendEmailCode(String email);
/**
* 绑定邮箱
* @return
*/
boolean bindEmail(String email,String code);
}
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private EmailService emailService;
@Resource
private RedisCache redisCache;
@Value("${custom.emailCode.expiration}")
private Long expiration;
@Resource
private UserMapper userMapper;
@Override
public boolean sendEmailCode(String email) {
// 定义存储键值对的键规则
String emailCodeKey = "emailCode:" + email;
// 从redis缓存中尝试获取验证码
String cacheCode = redisCache.getCacheObject(emailCodeKey);
String emailCode = "";
// 如果缓存中已经存在字符串数据,则直接取出
if(StringUtils.isBlank(cacheCode)){
// 随机生成6位验证码
emailCode = RandomUtil.randomNumbers(6);
// 将邮箱和验证码信息存入redis缓存
redisCache.setCacheObject(emailCodeKey,emailCode);
redisCache.expire(emailCodeKey,expiration*100);
}else{
// 缓存中已经存在数据,不重复生成,直接返回缓存数据(或者提示用户不要操作太频繁,确认邮箱后再重新尝试)
// emailCode = cacheCode;
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST,"验证码已生成并发送到您邮箱,请确认后再次尝试");
}
// 获取发送邮箱验证码的HTML模板(resources/templates/下存放模板信息)
TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
Template template = engine.getTemplate("email-code.ftl");
// 调用邮箱服务发送验证码信息
String subject = "【一人の境】邮箱验证码";
String content = template.render(Dict.create().set("code",emailCode).set("expiration", expiration));
emailService.sendMail(email,subject,content);
// 返回响应信息
return true;
}
/**
* 根据验证码、邮箱绑定当前用户邮箱信息
* @param email
* @param code
* @return
*/
@Override
public boolean bindEmail(String email, String code) {
// 校验当前指定的邮箱是否已被绑定(查看注册邮箱是否存在)
User findUser = userMapper.getUserVOByEmail(email);
ThrowUtils.throwIf(findUser != null,ErrorCode.USER_EMAIL_REPEAT_ERROR,"当前邮箱账号已被他人绑定,请确认后再次尝试");
// 获取缓存中的验证码信息
String emailCodeKey = "emailCode:" + email;
String cacheCode = redisCache.getCacheObject(emailCodeKey);
ThrowUtils.throwIf(StringUtils.isBlank(cacheCode),ErrorCode.VALID_CODE_ERROR,"当前验证码已过期,请稍后再次尝试");
// 校验验证码是否正确
ThrowUtils.throwIf(!cacheCode.equals(code),ErrorCode.VALID_CODE_ERROR,"验证码校验失败,请确认后再次尝试");
// 邮箱账号和验证码校验通过,更新邮箱信息
User user = new User();
user.setId(ShiroUtil.getCurrentUserId());
user.setUserEmail(email);
user.setUpdateTime(new Date());
int res = userMapper.updateById(user);
return res>0;
}
}
基于上述步骤,完善邮箱验证码逻辑,将其嵌入业务逻辑(发送邮箱验证码、后台验证邮箱验证码并进行换绑操作)
Powered by Waline v3.1.3