当前位置: 首页 > news >正文

JAVA-实战8 Redis实战项目—雷神点评(1)短信登录

僕たちのこと

雷神点评——短信登录

原理:基于Session实现

我们实现短信登录的原理为基于Session实现,具体实现的短信登录流程流程如下:
image

①用户提交手机号并校验,服务端校验通过则生成验证码并保留验证码到本地Session,然后服务端发送验证码给用户
②用户提交手机号和验证码,服务端校验验证码是否一致,校验通过则根据手机号查询用户,如果不存在则创建新用户并保存,然后保存用户手机号和验证码到本地Session
③用户每一次访问都需要校验登录状态,服务端从本地Session获取用户并判断是否存在,如果存在则保存用户到ThreadLocal并放行通过

配置文件信息如下:

server:port: 8080
spring:application:name: LeiShenCommentdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.100.128:3306/tcswuzbusername: rootpassword: 1234data:redis:host: 192.168.100.128port: 6379password: 1234lettuce:pool:max-wait: 10max-idle: 10min-idle: 1time-between-eviction-runs: 10sjackson:default-property-inclusion: non_null
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: truetype-aliases-package: org.example.pojo
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: truetype-aliases-package: org.example.pojo
logging:level:org.example: debug

发送短信验证码实现

发送短信验证码实现代码如下:

// 控制层代码
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("code")public ResultData SendCode(@RequestParam("phone") String PhoneNumber, HttpSession session) {return userService.UserSend(PhoneNumber,session);}
}// 服务层代码
@Slf4j
@Service
public class UserServiceImpl implements UserService {@Overridepublic ResultData UserSend(String PhoneNumber, HttpSession session) {// 校验手机号格式if(RegexUtils.isPhoneInvalid(PhoneNumber)) {return ResultData.error("The Format of the Phone Number is error!");}// 生成验证码String CheckCode = RandomUtil.randomNumbers(6);// Session存储验证码session.setAttribute("CheckCode",CheckCode);// 日志模拟验证码发送成功log.info("CheckCode Sends Successfully : {}",CheckCode);// 返回验证码return ResultData.success(CheckCode);}
}

这里校验电话号码以及邮箱地址等格式使用的工具类

public abstract class RegexPatterns {/*** 手机号正则*/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}$";/*** 邮箱正则*/public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";/*** 密码正则。4~32位的字母、数字、下划线*/public static final String PASSWORD_REGEX = "^\\w{4,32}$";/*** 验证码正则, 6位数字或字母*/public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
}public class RegexUtils {/*** 是否是无效手机格式* @param phone 要校验的手机号* @return true:符合,false:不符合*/public static boolean isPhoneInvalid(String phone){return mismatch(phone, RegexPatterns.PHONE_REGEX);}/*** 是否是无效邮箱格式* @param email 要校验的邮箱* @return true:符合,false:不符合*/public static boolean isEmailInvalid(String email){return mismatch(email, RegexPatterns.EMAIL_REGEX);}/*** 是否是无效验证码格式* @param code 要校验的验证码* @return true:符合,false:不符合*/public static boolean isCodeInvalid(String code){return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);}// 校验是否不符合正则格式private static boolean mismatch(String str, String regex){if (StrUtil.isBlank(str)) {return true;}return !str.matches(regex);}
}

测试效果如下:
image
image

实现短信验证的登录和注册功能

实现代码如下:

// 控制层代码
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/login")public ResultData Login(@RequestBody LoginFormData loginFormData,HttpSession session) {log.info("Now is Welcome:{} {}",loginFormData.getCode(),loginFormData.getPhone());return userService.UserLogin(loginFormData,session);}
}// 服务层代码
@Slf4j
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic ResultData UserLogin(LoginFormData loginFormData, HttpSession session) {// 校验用户电话号码格式String PhoneNumber = loginFormData.getPhone();if(RegexUtils.isPhoneInvalid(PhoneNumber)) {return ResultData.error("The Format of the Phone Number is error!");}// 查询用户验证码是否正确Object SessionCode = session.getAttribute("CheckCode");System.out.println(SessionCode);if(SessionCode==null || !SessionCode.toString().equals(loginFormData.getCode())) {return ResultData.error("CheckCode Error!");}// 根据电话号码查询用户是否存在LambdaQueryWrapper<UserData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(UserData::getPhone,PhoneNumber);UserData LoginUser = userMapper.selectOne(lambdaWrapper);// 如果不存在则根据电话号码生成新用户if(LoginUser == null) {LoginUser = CreateUserWithPhoneNumber(PhoneNumber);}// 将登录用户信息存入Sessionsession.setAttribute("LoginUser",LoginUser);return ResultData.success("Login Success! Welcome "+PhoneNumber);}// 生成新用户public UserData CreateUserWithPhoneNumber(String PhoneNumber) {UserData NewUser = new UserData();NewUser.setPhone(PhoneNumber);NewUser.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));userMapper.insert(NewUser);return NewUser;}
}

这里存储生成用户名公共前缀头使用的工具类

public class SystemConstants {public static final String IMAGE_UPLOAD_DIR = "D:\\lesson\\nginx-1.18.0\\html\\hmdp\\imgs\\";public static final String USER_NICK_NAME_PREFIX = "user_";public static final int DEFAULT_PAGE_SIZE = 5;public static final int MAX_PAGE_SIZE = 10;
}

测试效果如下:

测试已存在的用户登录
image
image

测试不存在的用户注册
image
image

登录校验拦截器

使用拦截器对每次操作进行登录状态校验,校验完成后使用ThreadLocal存储当前登录用户信息

// 拦截器配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LeiShenAccessInterceptor leishenInterceptor;// 添加拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {//拦截器拦截所有请求registry.addInterceptor(leishenInterceptor).addPathPatterns("/**").excludePathPatterns("/user/code","/user/login");}
}// 实现拦截器
@Slf4j
@Component
public class LeiShenAccessInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("开启拦截器请求");// 获取SessionHttpSession LoginSession = request.getSession();// 获取Session中的LoginUser信息Object LoginUser = LoginSession.getAttribute("LoginUser");// 如果不存在那么就是未登录,回复401状态码if(LoginUser==null) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}// 存入ThreadLocal线程CurrentHolder.setCurrentLocal((UserData) LoginUser);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {log.info("拦截方法彻底完成");// 在拦截方法彻底完成后释放相关资源CurrentHolder.removeCurrent();}}

ThreadLocal部分的代码实现

public class CurrentHolder {private static final ThreadLocal<UserData> CURRENT_LOCAL = new ThreadLocal<>();public static void setCurrentLocal(UserData NewUser) { //设置当前线程的userIDCURRENT_LOCAL.set(NewUser);}public static UserData getCurrent() { //获取return CURRENT_LOCAL.get();}public static void removeCurrent() { //删除CURRENT_LOCAL.remove();}
}

控制层测试方法

@GetMapping("/getme")
public ResultData GetNowUser() {UserData NowUser = CurrentHolder.getCurrent();log.info("Now User is {}",NowUser);return ResultData.success(NowUser);
}

测试效果如下:
image

隐藏用户敏感信息

每次登录时通过session.setAttribute("LoginUser",LoginUser);保存用户信息,但是如果存入信息过多会导致Session存储压力过大,同时也会暴露敏感信息,考虑只保留基本信息从而减缓Session存储压力并保证敏感信息安全

原本的User数据实体类

@TableName("tb_user")
public class UserData {@TableId(value = "id", type = IdType.AUTO)private Long id;private String phone;private String password;private String nickName;private String icon = "";private LocalDateTime createTime;private LocalDateTime updateTime;
}

只保留基本信息的数据实体类

public class UserBaseData {private Long id;private String nickName;private String icon = "";
}

修改后测试效果如下:
image

Redis替代Session

Session无法满足在多台Tomcat之间共享存储空间,使用Redis替代Session
image

使用Hash结构的Redis存储信息,更加灵活,可以针对单个字段进行CRUD操作,且占用内存较少

校验登录状态使用随机Token完成
image

修改发送短信验证码的方法,其中注意的点

①以电话号码作为key时需要添加统一前缀头login:code:
②验证码存在有效期,需要在存入redis时设置
③有效期和统一前缀头最好封装为全局常量

public ResultData UserSend(String PhoneNumber, HttpSession session) {if(RegexUtils.isPhoneInvalid(PhoneNumber)) {return ResultData.error("The Format of the Phone Number is error!");}String CheckCode = RandomUtil.randomNumbers(6);// 存入Redis时需要给key添加统一前缀头并设置验证码有效期stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+PhoneNumber,CheckCode,LOGIN_CODE_TTL, TimeUnit.MINUTES);log.info("CheckCode Sends Successfully : {}",CheckCode);return ResultData.success(CheckCode);
}

修改登录方法

@Override
public ResultData UserLogin(LoginFormData loginFormData) {String PhoneNumber = loginFormData.getPhone();if(RegexUtils.isPhoneInvalid(PhoneNumber)) {return ResultData.error("The Format of the Phone Number is error!");}// 校验验证码是否正确Object SessionCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+PhoneNumber);System.out.println(SessionCode);if(SessionCode==null || !SessionCode.toString().equals(loginFormData.getCode())) {return ResultData.error("CheckCode Error!");}LambdaQueryWrapper<UserData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(UserData::getPhone,PhoneNumber);UserData LoginUser = userMapper.selectOne(lambdaWrapper);if(LoginUser == null) {LoginUser = CreateUserWithPhoneNumber(PhoneNumber);}// 生成Token令牌并将LoginUser存入RedisString NewToken = UUID.randomUUID().toString();UserBaseData LoginUserBaseData = BeanUtil.toBean(LoginUser,UserBaseData.class);Map<String,Object> UserMap = BeanUtil.beanToMap(LoginUserBaseData);String TokenKey = LOGIN_USER_KEY+NewToken;// 存储stringRedisTemplate.opsForHash().putAll(TokenKey,UserMap);// 设置有效期stringRedisTemplate.expire(TokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);return ResultData.success("Login Success! Welcome "+PhoneNumber+" with Token: "+(NewToken));
}

在现实中,每一次访问都会刷新登录状态有效期(Token有效期),所以在拦截器中修改校验方式同时设置有效期刷新

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("开启拦截器请求");// 从请求头中获取Token令牌String UserToken = request.getHeader("Token");if(UserToken == null || UserToken.isEmpty()) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}// 根据Token令牌从Redis中获取登录用户信息Map<Object,Object> UserMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY+UserToken);// 如果登录用户不存在或者为空则返回401状态码if(UserMap==null||UserMap.isEmpty()) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}// 将当前用户存入ThreadLocalUserBaseData LoginUser = new UserBaseData();BeanUtil.fillBeanWithMapIgnoreCase(UserMap,LoginUser,false);System.out.println(LoginUser);CurrentHolder.setCurrentLocal(LoginUser);// 刷新有效期String TokenKey = LOGIN_USER_KEY+UserToken;stringRedisTemplate.expire(TokenKey,LOGIN_USER_TTL, TimeUnit.MINUTES);return true;
}

使用工具类RedisConstants封装Redis相关常量

public class RedisConstants {public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 3L;public static final String LOGIN_USER_KEY = "login:token:";public static final Long LOGIN_USER_TTL = 10L;
}

进行测试:

首先发送验证码:

image
查看Redis数据库

image
使用验证码进行登录并获取Token

image
查看Redis数据库

image
使用Token访问
image

拦截器优化-状态登录刷新

优化拦截器设置,如果已登录那么全部的访问都会刷新登录状态
image

重新修改拦截器类:

相较于之前的拦截器,刷新拦截器LeiShenRefreshInterceptor只负责刷新状态,无论结果如何访问全部过滤到下一个登录拦截器LeiShenLoginInterceptor,因此判断全部return true;
刷新拦截器LeiShenRefreshInterceptor中如果Token存在且合法那么会将用户信息存入ThreadLocal,因此登录拦截器LeiShenLoginInterceptor只需要校验ThreadLocal中是否保存用户信息即可判断访问会否合法

@Slf4j
@Component
public class LeiShenRefreshInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("开启刷新拦截器请求");String UserToken = request.getHeader("Token");if(UserToken == null || UserToken.isEmpty()) {return true;}Map<Object,Object> UserMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY+UserToken);if(UserMap==null||UserMap.isEmpty()) {return true;}UserBaseData LoginUser = new UserBaseData();BeanUtil.fillBeanWithMapIgnoreCase(UserMap,LoginUser,false);System.out.println(LoginUser);CurrentHolder.setCurrentLocal(LoginUser);String TokenKey = LOGIN_USER_KEY+UserToken;stringRedisTemplate.expire(TokenKey,LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {log.info("拦截方法彻底完成");CurrentHolder.removeCurrent();}
}@Slf4j
@Component
public class LeiShenLoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if(CurrentHolder.getCurrent()==null) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}return true;}}

配置类修改如下,使用registryorder方法设置拦截器优先级,保证刷新拦截器和登录拦截器的执行顺序:

@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LeiShenLoginInterceptor leishenLoginInterceptor;@Autowiredprivate LeiShenRefreshInterceptor leiShenRefreshInterceptor;// 添加拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {//拦截器拦截所有请求registry.addInterceptor(leishenLoginInterceptor).addPathPatterns("/**").excludePathPatterns("/shop/**","/voucher/**","/shop_type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);registry.addInterceptor(leiShenRefreshInterceptor).addPathPatterns("/**").order(0);}
}

http://www.jsqmd.com/news/741251/

相关文章:

  • 2026年4月饮品加盟市场盘点:为何执着饮品成为热门选择? - 2026年企业推荐榜
  • 9 【自适应天线与相控阵技术】单极子相控阵天线的设计、分析与测试:从有限阵列矩量法到无限阵列 Floquet 理论的完整推导
  • OpenClaw部署助手:零代码一键部署AI智能体网关的实践指南
  • 构建支持多 AI 模型的智能客服系统架构设计与接入实践
  • 现在不建立编译器适配测试基线,明年Rust/C++23混合编译项目将触发不可逆的ABI断裂——资深编译器工程师的3条生存建议
  • 遥感解译效率提升83%的秘密,全开源Python工具包首次公开:支持SAR、多光谱、高光谱的端到端AI解译工作流
  • R语言数据分析第一步:别再只会用summary()看平均数了,这5个隐藏用法帮你快速定位数据问题
  • 2026年太原全屋整装设计优选:索菲亚全屋定制旗舰店深度解析 - 2026年企业推荐榜
  • QQ音乐加密文件终极解密指南:5分钟学会本地无损转换
  • 别再只用话题和服务了!用ROS2 Action实现带进度反馈的机器人任务控制(附小乌龟实战)
  • 2026年至今,矿山设备行业如何甄选可靠伙伴?甲诚矿机以硬实力赢得口碑 - 2026年企业推荐榜
  • PyTorch在TVA系统中的关键作用(4)
  • 鸣潮智能辅助:解放双手的后台自动化助手
  • Get cookies.txt LOCALLY:浏览器Cookie本地安全导出终极指南
  • 从硬件到代码:手把手拆解DMA外挂的完整工作流(以Apex为例)
  • 2026年5月正规的重庆火锅底料代工生产如何选厂家推荐榜,经典牛油型清油型定制型厂家选择指南 - 海棠依旧大
  • 【C语言RTOS优化黄金法则】:20年嵌入式老兵亲授5大内存泄漏根治技巧与实时性提升37%的硬核实践
  • 2026年Q2秦皇岛全屋定制供货商深度**:维饰立凭何成为智造首选? - 2026年企业推荐榜
  • 5个创新方法提升你的网盘下载效率:LinkSwift直链解析工具深度解析
  • 跨设备角色迁移:3步完成艾尔登法环存档无损转移
  • 个人开发者如何利用Taotoken以更低成本体验全球主流大模型
  • 别再手动改Word了!用Python的python-docx库批量生成报告,5分钟搞定周报
  • 从Activity销毁看协程生命周期:用lifecycleScope和ViewModelScope优化你的Kotlin代码
  • 保姆级教程:在Gazebo仿真和真实TurtleBot3上,手把手调试Hector SLAM的3个关键参数
  • 开发者在实际项目中如何组合使用Taotoken的不同模型
  • 2026年降AI工具改写自然度横评:五款工具改写后可读性和文风保留度对比
  • RTOS任务调度器性能瓶颈在哪?揭秘C语言层3类隐式阻塞代码及4步零抖动优化法
  • 中美空运物流哪家口碑好? - 恒盛通物流
  • 医学图像分割实战:基于TransUNet训练自己的眼底硬渗出物数据集(附完整代码)
  • 别再傻傻分不清!嵌入式C语言面试必问的6个基础概念(附避坑指南)