基于Redis实现短信验证码登录项目示例(附源码)
本篇文章给大家分享《基于Redis实现短信验证码登录项目示例(附源码)》,覆盖了数据库的常见基础知识,其实一个语言的全部知识点一篇文章是不可能说完的,但希望通过这些问题,让读者对自己的掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握它。
Redis短信登录流程描述
短信验证码的发送
用户提交手机号,系统验证手机号是否有效,毕竟无效手机号会消耗你的短信验证次数还会导致系统的性能下降。如果手机号为无效的话就让用户重新提交手机号,如果有效就生成验证码并将该验证码作为value保存到redis中对应的key是手机号,之所以这么做的原因是保证key的唯一性,如果使用固定字符串作为可以的话会被后面的数据所覆盖。然后在控制台输出验证码模拟发送验证码的过程
短信验证码的验证
用户的手机号接收到验证码后在平台上提交验证码,系统从redis中根据手机号读取验证码并进行校验,如果验证通过的话就根据用户验证使用的手机号去数据库中进行查询用户信息。如果存在就将查询到的用户信息保存到redis中,完成登录;如果不存在的话就创建一个新用户,并将该用户的信息分别保存到sql数据库和redis中,生成随机token作为key、使用hash结构存储user数据作为value,并将这个token返回给客户端,至此完成登录注册
是否登录的验证
用户访问系统业务逻辑的时候需要校验他是否已经登录,如果登录可以访问否则就去登录,那么该如何完成是否登录的校验呢?这就要了解session的相关知识了,每一个session都有一个sessionId信息保存在浏览器的cookie中,当用户使用浏览器发送请求的时候会携带上cookie信息,此时系统就可以使用cookie中的sessionId获取到session信息,并通过session获取到登录时存储的用户信息。如果此时用户在数据库中存在的话就将该用户的信息缓存在ThreadLocal(方便后续验证)中,并放行该访问;否则就说明发送请求的用户未登录或不合法,就要拦截到他的请求前往登录
源码分析
模拟发送短信验证码
UserController定义与前端交互
@Resource
private IUserService userService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
上面使用到了sendCode方法,在userService里定义一下接口,然后在对应实现类中按照上面的流程重写该方法的业务逻辑代码
@Override
public Result sendCode(String phone, HttpSession session) {
// 校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 无效手机号,返回错误信息
return Result.fail("手机号格式有误!");
}
// 有效生成验证码
String code = RandomUtil.randomNumbers(6);
// 保存 (固定前缀+手机号) 和验证码到Redis中,设置验证码的有效期为2分钟
// RedisConstants.LOGIN_CODE_KEY = “login:code:”
// RedisConstants.LOGIN_CODE_TTL = 2L
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 模拟发送验证码
log.debug("验证码:{}", code);
// 返回
return Result.ok();
}
手机号格式校验使用到的RegexUtils类中的工具方法
/**
* 手机号正则
*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
/**
* 是否是无效手机格式
* @param phone 要校验的手机号
* @return true:符合,false:不符合
*/
public static boolean isPhoneInvalid(String phone){
return mismatch(phone, RegexPatterns.PHONE_REGEX);
}
// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){
if (StrUtil.isBlank(str)) {
return true;
}
return !str.matches(regex);
}
短信验证码的验证
UserController定义与前端交互,其中参数LoginFormDTO 是前端使用手机号+验证码登录或者手机号+密码登录是传递过来的JSON数据
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
上面使用到了login方法,在userService里定义一下接口,然后在对应实现类中按照上卖弄的流程描述重写该方法的业务逻辑代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
// 验证码校验
String code = loginForm.getCode();
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
if (cacheCode == null || !code.equals(cacheCode)) {
return Result.fail("验证码错误!");
}
// 根据手机号查询用户信息
User user = query().eq("phone", phone).one();
if (user == null) {
// 不存在就创建一个新用户
user = createUserWithPhone(phone);
}
// 保存用户信息到redis中
// 生成随机token
String token = UUID.randomUUID().toString(true);
// user先转userDTO再转hashMap存储 转HashMap时的第三个参数的意思是忽略null值将值都转换成String类型
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<string object> userMap = BeanUtil.beanToMap(userDTO, new HashMap(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// RedisConstants.LOGIN_USER_KEY = "login:token:"
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
// 设置失效时间为30分钟
// RedisConstants.LOGIN_USER_TTL = 30L
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 返回前端token
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
// SystemConstants.USER_NICK_NAME_PREFIX = "user_"
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
save(user);
return user;
}</string>
保存的时候使用BeanUtil将User转换成UserDTO进行存储,UserDTO的结构如下,只保存一部分的数据,一方面可以不用来回传递用户有关的隐私数据,一方面也节省内存提高性能。由于这里的id是数值类型,但是stringRedisTemplate存储时需要hash的键值都是String型,所以说应该在存储之前将id的值转换成String类型,就在上面代码块的24~27行完成了这个操作
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
校验是否登录
用户发送请求不止一次,所以说登录验证也不止进行一次,于是可以使用拦截器完成验证,拦截器的使用可分为两步:
创建拦截器
/**
* @author : mereign
* @date : 2022/5/5 - 10:31
* @desc : 拦截器,实现请求拦截,判断登录信息
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的token信息
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// token为空,返回401未授权状态码,拦截
response.setStatus(401);
return false;
}
// 根据token获取redis中的用户value
Map<object object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
HttpSession session = request.getSession();
// 判断用户是否存在
if (userMap.isEmpty()) {
// 用户不存在,返回401未授权状态码,拦截
response.setStatus(401);
return false;
}
// 用户存在,将hash数据转换为userDTO,存信息到ThreadLocal
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(userDTO);
// 刷新token有效期,放行
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}</object>
注册拦截器
/**
* @author : mereign
* @date : 2022/5/5 - 10:43
* @desc :
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.excludePathPatterns(
"/shop/**",
"/shop-type/**",
"/voucher/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
缓存用户的信息到ThreadLocal中的工具方法
public class UserHolder {
private static final ThreadLocal<userdto> tl = new ThreadLocal();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}</userdto>
UserController定义与前端交互
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
登录验证优化
由上面的登录验证可知,我们对一些需要用户登录验证的功能设置了拦截器,如果验证通过会刷新token的有效期,这样的话只要用户一直访问我们拦截的功能就可以一直保持token是有效的。但是,如果用户登陆之后的操作一直是不需要验证的,那也就意味着token的有效期一直不会刷新,这样的话30分钟之后token就会失效用户验证就会失败,这样显然是不合理的
于是我们可以使用两个拦截器完成,最前面的负责拦截所有的请求,获取token、从redis中查询用户,将查询结果放到ThreadLocal(可能存null)、刷新token有效期,最后直接放行;后面的拦截器只负责判断有没有从redis中查询到用户,他从ThreadLocal获取查询结果,判断有则放行无则拦截
创建两个拦截器
/**
* @author : mereign
* @date : 2022/5/5 - 10:31
* @desc : 前置拦截器,拦截所有请求,前置工作
*/
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的token信息
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// token为空 直接放行
return true;
}
// 根据token获取redis中的用户value
Map<object object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
HttpSession session = request.getSession();
// 判断用户是否存在
if (userMap.isEmpty()) {
// 用户不存在 直接放行
return true;
}
// 用户存在,将hash数据转换为userDTO,存信息到ThreadLocal
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(userDTO);
// 刷新token有效期,放行
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}</object>
/**
* @author : mereign
* @date : 2022/5/5 - 10:31
* @desc : 登录拦截器,拦截需要拦截的请求,判断登录信息
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断登录
if (UserHolder.getUser() == null) {
response.setStatus(401);
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
创建完拦截器之后要将两个拦截器通过配置类配置到容器中生效,多个拦截器的优先级,默认按照添加顺序执行优先级,但是也可以使用order方法指定优先级,按参数的大小排序优先级,参数越小优先级越高
/**
* @author : mereign
* @date : 2022/5/5 - 10:43
* @desc : 配置类注册拦截器
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private RefreshTokenInterceptor refreshTokenInterceptor;
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 前置拦截器
registry.addInterceptor(refreshTokenInterceptor)
.addPathPatterns("/**")
.order(0);
// 后置拦截器
registry.addInterceptor(loginInterceptor)
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
)
.order(1);
}
}
以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于数据库的相关知识,也可关注golang学习网公众号。
Redis中ServiceStack.Redis和StackExchange.Redis区别详解
- 上一篇
- Redis中ServiceStack.Redis和StackExchange.Redis区别详解
- 下一篇
- 设置Redis最大占用内存的实现
-
- 数据库 · Redis | 9小时前 |
- 监控Redis集群健康状态的工具与指标
- 112浏览 收藏
-
- 数据库 · Redis | 1星期前 |
- Redis数据安全防护全攻略
- 252浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- Redis主从复制故障排查与修复技巧
- 302浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- Redis与HBase存储方案详解
- 325浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- Redis数据安全防护全攻略
- 157浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- 高并发Redis优化技巧分享
- 257浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- Redis数据安全防护全攻略
- 398浏览 收藏
-
- 数据库 · Redis | 3星期前 |
- Redis配置加密方法与安全设置
- 232浏览 收藏
-
- 数据库 · Redis | 3星期前 |
- RedisHyperLogLog高效统计技巧
- 283浏览 收藏
-
- 数据库 · Redis | 3星期前 |
- Redis与MySQL缓存同步方法详解
- 141浏览 收藏
-
- 数据库 · Redis | 3星期前 |
- Redis布隆过滤器防穿透原理解析
- 312浏览 收藏
-
- 数据库 · Redis | 1个月前 |
- Redis容器化部署实战技巧分享
- 195浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3164次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3376次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3405次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4507次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3785次使用
-
- MySQL 生成随机数字、字符串、日期、验证码及 UUID的方法
- 2023-01-07 117浏览
-
- 源码解析gtoken替换jwt实现sso登录
- 2022-12-23 194浏览
-
- 如何基于Session实现短信登录功能
- 2023-01-01 401浏览
-
- go使用Gin框架利用阿里云实现短信验证码功能
- 2023-01-07 402浏览
-
- Golang Http 验证码示例实现
- 2023-01-07 140浏览

