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

用户注册这样玩,保你平安

前言

基本上每个系统系统都包含用户注册、发送验证码等基本操作。在前些年,我还记得我在逛 csdn、贴吧、网易新闻等网站的时候是可以不登陆也能浏览完网页内容的,但是近几年这些网站已经改成了不登陆不让用,浏览网页时不时提醒你要进行登录,对于一些不喜欢注册的用户造成了相当大的困扰。

但是不知道大家有没有想过这里面的深层逻辑,就是为什么前些年什么 csdn、贴吧、网易新闻等明明不进行登录浏览网页体验还行,现在要改成这样子?

这里面涉及的因素有很多,比如互联网发展到头、变现困难、存量环境加剧内卷等。

当公司盈利压力变大,老板眼看收益日趋降低,便开始拉领导开会,领导开完会开始 PUA 员工,一层一层递进,辅以绩效、okr 等工具制定目标结果。于是公司底层员工的想法从努力赚钱、升职加薪变成保住饭碗、养活一家老小,对于业务上的月度、季度营收要求自然是各种促进用户付费的手段应上齐上。

这里面提升付费有一个非常重要的前提就是用户,只要有了用户就有付费希望。

如果用户不注册,不留下手机号、邮箱等个人信息,互联网运营又怎么给这些用户发送营销短信和邮件。所以说强制注册本质上是为了公司利益。

只要把用户留下来,留在自己的 APP 里,收集用户信息,后续各种运营活动、支付弹窗、短信找回、活动抽奖一起上,何愁没有用户 😜。

用户信息记录的意义是为了聚集 C 端用户、收集信息,为后续运营活动(提升付费)做准备。就拿淘宝举例,个性化推荐、千人千面、双 11 活动等,这一系列运营活动说到底都是为了提升淘宝的付费金额,提升淘宝平台的 GMV。什么个性化推荐、千人千面说白了就是收集你的个人信息,你的商品点击、浏览、下单等操作都会被淘宝采集,进而通过算法模型进行商品推荐,选出你可能感兴趣的商品展示,从而提升淘宝付费金额。

OK,到这里题外话说多了,虽然说用户注册是一个很基本的逻辑,但是很多人一不小心就会掉坑里。这里我给大家介绍下 waynboot-mall 项目中用户注册是怎么玩的,为什么说可以保你平安。

waynboot-mall 项目是由我开源的一套 H5 商城项目,包含运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0 框架开发而来,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。商城模块划分合理、代码质量较高、易于部署,非常适合大家拿来学习使用。

github 地址:https://github.com/wayn111/waynboot-mall

用户注册

在 waynboot-mall 项目中,商城注册页面截图如下。

/captcha 生成图形验证码接口

@ResponseBody
@RequestMapping("/captcha")
public R captcha() {// 1. 创建验证码对象,定义验证码图形的长、宽、以及字数SpecCaptcha specCaptcha = new SpecCaptcha(80, 32, 4);// 2. 生成验证码String verCode = specCaptcha.text().toLowerCase();// 3. 生成验证码唯一keyString captchaKey = IdUtil.getUid();// 4. 存入redis并设置过期时间为30分钟redisCache.setCacheObject(captchaKey, verCode, SysConstants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);// 5. 将key和base64返回给前端return R.success().add("captchaKey", captchaKey).add("image", specCaptcha.toBase64());
}

验证码接口基本是每个系统都有的接口,验证码主要是为了防止别人直接调用接口进行注册操作,是一个安全措施。现在市面上流行的有图形验证码、滑块验证码、点选验证码等,waynboot-mall 项目中使用的图形验证码,大家有兴趣可以了解 tianai-captcha 这个项目,包含滑块验证码、点选验证码等。现在我们对验证码接口进行讲解,

  • 第一步,创建验证码对象,定义验证码图形的长、宽、以及字数(这里创建的 SpecCaptcha 对象来自 easy-captcha 项目)

  • 第二步,生成验证码 verCode

  • 第三步,为验证码生成唯一 captchaKey

  • 第四步,将 captchaKey 作为 key, verCode 作为 value,存入 redis 并设置过期时间

  • 第五步,将 captchaKey 以及验证码图像的 base64 编码返回给前端

前端在调用完 /captcha 接口后,会拿到 captchaKey 以及验证码图像的 base64 编码,之后前端就可以将 base64 编码作为 img 标签 src 属性用作图形验证码展示。

用户输入邮箱和图形验证码后就可以点击发送邮箱验证码了。

调用发送邮箱验证码接口时会将 captchaKey、验证码、手机号等信息一起传给服务端。

/sendEmailCode 发送邮箱验证码接口

@PostMapping("/sendEmailCode")
public R sendEmailCode(@RequestBody RegistryObj registryObj) {String captchaKey = registryObj.getCaptchaKey();String captchaCode = registryObj.getCaptchaCode();String mobile = registryObj.getMobile();if (StringUtils.isBlank(captchaKey)) {return R.error(CUSTOM_ERROR.setMsg("图形验证码错误"));}if (StringUtils.isBlank(captchaCode)) {return R.error(CUSTOM_ERROR.setMsg("图形验证码为空"));}if (StringUtils.isBlank(mobile)) {return R.error(CUSTOM_ERROR.setMsg("手机号为空"));}String redisCode = redisCache.getCacheObject(captchaKey);// 判断验证码codeif (!redisCode.equals(captchaCode.trim().toLowerCase())) {return R.error(USER_CAPTCHA_CODE_ERROR);}// 验证手机号是否唯一long count = iMemberService.count(Wrappers.lambdaQuery(Member.class).eq(Member::getMobile, mobile));if (count > 0) {return R.error(USER_PHONE_HAS_REGISTER_ERROR);}// 生成邮箱验证码codeString emailCode = RandomUtil.randomString(6);// 生成邮箱验证码唯一keyString emailKey = RedisKeyEnum.EMAIL_KEY_CACHE.getKey(IdUtil.getUid());// 存入redis并设置过期时间为20分钟redisCache.setCacheObject(emailKey, emailCode + "_" + mobile,  RedisKeyEnum.EMAIL_KEY_CACHE.getExpireSecond());commonThreadPoolTaskExecutor.execute(() -> {EmailConfig emailConfig = mailConfigService.getById(1L);SendMailVO sendMailVO = new SendMailVO();sendMailVO.setSubject("mall商城注册通知");sendMailVO.setContent("邮箱验证码:" + emailCode);sendMailVO.setTos(Collections.singletonList(registryObj.getEmail()));MailUtil.sendMail(emailConfig, sendMailVO, false, false);});return R.success().add("emailKey", emailKey);
}

一般商城系统中,发送邮箱验证码、短信验证码时都需要进行验证码输入这一步骤,这是为了防止别人直接通过接口调用的形式,浪费我们系统的资源,特别是发送手机验证码、邮件这种资源。发送邮箱验证码接口讲解如下,

  • 第一步,校验 captchaKey、captchaCode、mobile 必传参数

  • 第二步,根据 captchaKey 读取 redis 中存放的验证码 code,与用户输入 captchaCode 进行比较

  • 第三步,验证用户手机号是否唯一

  • 第四步,生成六位邮箱验证码 emailCode

  • 第五步,生成邮箱验证码唯一 emailKey

  • 第六步,将 emailKey 作为 key, emailCode_mobile 作为 value,存入 redis 并设置过期时间(注意这一步将用户手机号,也存入 Redis 是为了防止用户在获取完邮箱验证码后修改手机号,这一点很重要,很多开发同学都忘了这一步)

  • 第七步,使用线程池异步发送验证码邮件

前端在调用完 /sendEmailCode 接口后,就可以拿到 emailKey。

这样等用户输入邮箱里的验证码后,点击注册按钮,我们就可能正式开始注册操作了。

/registry 用户注册

@PostMapping("/registry")
public R registry(@RequestBody RegistryObj registryObj) {// 验证两次密码输入是否一致if (!StringUtils.equalsIgnoreCase(registryObj.getPassword(), registryObj.getConfirmPassword())) {return R.error(USER_TWO_PASSWORD_NOT_SAME_ERROR);}// 验证用户手机号是否唯一long count = iMemberService.count(Wrappers.lambdaQuery(Member.class).eq(Member::getMobile, registryObj.getMobile()));if (count > 0) {return R.error(USER_PHONE_HAS_REGISTER_ERROR);}// 判断图形验证码String redisCaptchaCode = redisCache.getCacheObject(registryObj.getCaptchaKey());if (registryObj.getCaptchaCode() == null || !redisCaptchaCode.equals(registryObj.getCaptchaCode().trim().toLowerCase())) {return R.error(USER_CAPTCHA_CODE_ERROR);}// 判断邮箱验证码String value = redisCache.getCacheObject(registryObj.getEmailKey());String[] split = value.split("_");if (split.length < 2) {return R.error(ReturnCodeEnum.USER_EMAIL_CODE_ERROR);}String redisEmailCode = split[0];String mobile = split[1];// 判断发送邮箱验证码的手机号是否与用户当前传入手机号一致if (!StringUtils.equalsIgnoreCase(mobile, registryObj.getMobile())) {return R.error(ReturnCodeEnum.USER_REGISTER_MOBILE_ERROR);}// 判断用户输入邮箱验证码是否正确if (registryObj.getEmailCode() == null || !redisEmailCode.equals(registryObj.getEmailCode().trim().toLowerCase())) {return R.error(ReturnCodeEnum.USER_EMAIL_CODE_ERROR);}// 删除验证码redisCache.deleteObject(registryObj.getCaptchaKey());redisCache.deleteObject(registryObj.getEmailKey());Member member = new Member();long time = System.currentTimeMillis();member.setNickname("昵称" + time / 1000);String avatar = SysConstants.DEFAULT_AVATAR;member.setAvatar(avatar);member.setMobile(registryObj.getMobile());member.setEmail(registryObj.getEmail());member.setPassword(SecurityUtils.encryptPassword(registryObj.getPassword()));member.setCreateTime(new Date());return R.result(iMemberService.save(member));
}

注册接口,需要逻辑完善,所以这里的校验逻辑会比较多,因为一个商城最重要的几个接口就是注册、登录、下单、支付等。

除了能让用户正常注册外,有时候还需要确保用户一个手机号只能注册一个账号,完成对用户手机号在商城的唯一性保障。除了先查询用户手机号是否已存在外,还需要对用户 member 表的手机号字段设置唯一索引来完成。注册接口讲解如下,

唯一索引可以防止用户重复点击注册按钮,保证一个手机号只能注册一个用户。

  • 第一步,验证用户输入两次密码是否一致

  • 第二步,验证用户输入的手机号是否唯一

  • 第三步,验证用户输入的图形验证码是否于 Redis 中存储一致

  • 第四步,验证发送邮箱验证码的手机号是否于 Redis 中存储一致

  • 第五步,验证用户输入的邮箱验证码是否于 Redis 中存储一致

  • 第六步,校验通过,开始删除图形验证码、邮箱验证码

  • 第七步,启动线程池,异步进行用户保存操作

最后聊两句

用户注册说简单是很简单,但是校验逻辑一定要做好!这是我的踩坑经验,现在我传授给你,希望能帮你平安🤝。

关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、国外优质文章翻译等,您的关注将是我的更新动力!

相关文章:

用户注册这样玩,保你平安

前言 基本上每个系统系统都包含用户注册、发送验证码等基本操作。在前些年&#xff0c;我还记得我在逛 csdn、贴吧、网易新闻等网站的时候是可以不登陆也能浏览完网页内容的&#xff0c;但是近几年这些网站已经改成了不登陆不让用&#xff0c;浏览网页时不时提醒你要进行登录&…...

QXDM Filter使用指南

QXDM Filter使用指南 1. QXDM简介2 如何制作和导入Filter2.1 制作Filter2.1.1 制作Windows环境下Filter2.1.2 制作Linux环境下Filter 2.2 Windows环境下导入Filter 3 Filter配置3.1 注册拨号问题3.1.1 LOG Packets(OTA)3.1.2 LOG Packets3.1.3 Event Reports3.1.4 Message Pack…...

智能优化算法应用:基于蝴蝶算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于蝴蝶算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于蝴蝶算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.蝴蝶算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…...

3dsMax插件Datasmith Exporter安装使用方法

3dsMax插件Datasmith Exporter安装使用方法 某些文件格式无法用Datasmith直接导入虚幻引擎&#xff0c;这些数据必须先被转换为Datasmith能够识别的文件格式。Datasmith Exporter插件就可以帮助您的软件导出可以被Datasmith导入虚幻引擎的.udatasmith格式文件。 在开始使用虚幻…...

Rocketmq架构

NameServer&#xff1a;作为注册中心&#xff0c;提供路由注册、路由踢出、路由发现功能&#xff0c;舍弃强一致&#xff0c;保证高可用&#xff0c;集群中各个节点不会实时通讯&#xff0c;其中一个节点下线之后&#xff0c;会提供另外一个节点保证路由功能。 Rocket mq name…...

中兴亮相中国国际现代化铁路技术装备展览会 筑智铁路5G同行

近日&#xff0c;第十六届中国国际现代化铁路技术装备展览会在北京中国国际展览中心举办&#xff0c;中兴以“数智铁路&#xff0c;5G同行”主题亮相本次展览会&#xff0c;并全面展示了“数字铁路网络基础设施”、“云边结合的铁路行业云”、“数字铁路赋能赋智”等方面的最新…...

从零学算法15

15.给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可以包含重复的三元组。 示例 …...

《Effective C++》条款27

尽量少做转型动作 class A { public:A(int x) :a(x) {};virtual void add(){a;} private:int a; }; class B :public A { public:B(int x) :b(x),A(x) {};virtual void add(){static_cast<A>(*this).add();b;}private:int b; }; 如上描述把子类转型为A类&#xff0c;调用…...

无图谱不AI之三元组数据保存Neo4j

目录 1. 代码学习1.1 源代码1.2 代码解读 没有图谱称为弱人工智能&#xff0c;有图谱的称为强人工智能。 图谱可以让机器学习和人工智能具备推理能力。 1. 代码学习 1.1 源代码 # -*- coding: utf-8 -*- from py2neo import Node, Graph, Relationship# 可以先阅读下文档&a…...

Mybatisplus同时向两张表里插入数据[事务的一致性]

一、需求&#xff1a;把靶器官的数据&#xff0c;单独拿出来作为一个从表&#xff0c;以List的方式接收这段数据&#xff1b; 此时分析&#xff0c;是需要有两个实体的&#xff0c;一个是主表的实体&#xff0c;一个是从表的实体&#xff0c;并在主表实体新增一个List 字段来接…...

天眼销:精准的企业名录

企业名录的重要性&#xff0c;对于销售而言都是极其重要的。本期为家人们分享如何正确挑选出优质的企业名录渠道&#xff0c;避免走一些弯弯坑坑。 为了有效利用企业名录进行客户开发&#xff0c;您需要关注信息的准确性、可提供的资源数量以及信息的时效性。能否根据您的需求…...

TypeError: Cannot read properties of null (reading ‘shapeFlag‘)

vue3 开发过程遇到这样一个报错 TypeError: Cannot read properties of null (reading shapeFlag)最后发现是ref定义的变量&#xff0c;在访问时没有使用.valuereactive 变量初始化是数组&#xff0c;如果使用字符串赋值时也会报这个错。...

视频监控平台EasyCVR多场景应用,AI视频分析技术助力行业升级转型

传统的视频监控系统建设&#xff0c;经常存在各方面的因素制约&#xff0c;造成管理机制不健全、统筹规划不到位、联网共享不规范&#xff0c;形成“信息孤岛”、“数据烟囱”。在监控系统的建设中缺乏统一规划&#xff0c;标准不统一、视频图像信息利用率低等问题日益突出。随…...

如何搭建外网可访问的Serv-U FTP服务器,轻松远程共享文件!

目录 1. 前言 2. 本地FTP搭建 2.1 Serv-U下载和安装 2.2 Serv-U共享网页测试 2.3 Cpolar下载和安装 3. 本地FTP发布 3.1 Cpolar云端设置 3.2 Cpolar本地设置 4. 公网访问测试 5. 总结 1. 前言 科技日益发展的今天&#xff0c;移动电子设备似乎成了我们生活的主角&am…...

c++--类型的基础

1.常量对象&#xff0c;常量成员函数 (1).常量对象 常量对象的引用和指针不能调用类的普通的成员函数。只能调用常量成员函数。 (2).常量成员函数&#xff1a;把const放在类成员函数参数列表后。表示隐含的this是一个指向常量的指针 (3).当创建一个const对象时&#xff0c;直到…...

Python with提前退出:坑与解决方案

Python with提前退出&#xff1a;坑与解决方案 问题的起源 早些时候使用with实现了一版全局进程锁&#xff0c;希望实现以下效果&#xff1a; Python with提前退出&#xff1a;坑与解决方案 全局进程锁本身不用多说&#xff0c;大部分都依靠外部的缓存来实现的&#xff0c;r…...

Vue3-provide和inject

作用和场景&#xff1a;顶层组件向任意的底层组件传递数据和方法&#xff0c;实现跨层组件通信 跨层传递普通数据&#xff1a; 1.顶层组件通过provide函数提供数据 2.底层组件通过inject函数获取数据 既可以传递普通数据&#xff0c;也可以使用ref传递响应式数据&#xff08…...

Python与设计模式--适配器模式

23种计模式之 前言 &#xff08;5&#xff09;单例模式、工厂模式、简单工厂模式、抽象工厂模式、建造者模式、原型模式、(7)代理模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式、桥梁模式、&#xff08;11&#xff09;策略模式、责任链模式、命令模式、中介者模…...

大模型能否生成搜索引擎的未来?

文&#xff5c;郝 鑫 编&#xff5c;刘雨琦 ChatGPT火爆之前&#xff0c;水面下&#xff0c;也有中国公司也在朝着智能助手的方向努力。夸克便是其中之一。在GPT风靡科技圈后&#xff0c;国内就开始陆续冒出一些大模型厂商。对当时夸克而言&#xff0c;做大模型毋庸置疑&am…...

鸿蒙开发-ArkTS 语言-状态管理

[写在前面: 文章多处用到gif动图&#xff0c;如未自动播放&#xff0c;请点击图片] 衔接上一篇:鸿蒙开发-ArkTS 语言-基础语法 3. 状态管理 变量必须被装饰器装饰才能成为状态变量&#xff0c;状态变量的改变才能导致 UI 界面重新渲染 概念描述状态变量被状态装饰器装饰的变…...

谷歌浏览器插件

项目中有时候会用到插件 sync-cookie-extension1.0.0&#xff1a;开发环境同步测试 cookie 至 localhost&#xff0c;便于本地请求服务携带 cookie 参考地址&#xff1a;https://juejin.cn/post/7139354571712757767 里面有源码下载下来&#xff0c;加在到扩展即可使用FeHelp…...

使用分级同态加密防御梯度泄漏

抽象 联邦学习 &#xff08;FL&#xff09; 支持跨分布式客户端进行协作模型训练&#xff0c;而无需共享原始数据&#xff0c;这使其成为在互联和自动驾驶汽车 &#xff08;CAV&#xff09; 等领域保护隐私的机器学习的一种很有前途的方法。然而&#xff0c;最近的研究表明&…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...

算法岗面试经验分享-大模型篇

文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer &#xff08;1&#xff09;资源 论文&a…...

【网络安全】开源系统getshell漏洞挖掘

审计过程&#xff1a; 在入口文件admin/index.php中&#xff1a; 用户可以通过m,c,a等参数控制加载的文件和方法&#xff0c;在app/system/entrance.php中存在重点代码&#xff1a; 当M_TYPE system并且M_MODULE include时&#xff0c;会设置常量PATH_OWN_FILE为PATH_APP.M_T…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)

引言 在人工智能飞速发展的今天&#xff0c;大语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为技术领域的焦点。从智能写作到代码生成&#xff0c;LLM 的应用场景不断扩展&#xff0c;深刻改变了我们的工作和生活方式。然而&#xff0c;理解这些模型的内部…...

MySQL 主从同步异常处理

阅读原文&#xff1a;https://www.xiaozaoshu.top/articles/mysql-m-s-update-pk MySQL 做双主&#xff0c;遇到的这个错误&#xff1a; Could not execute Update_rows event on table ... Error_code: 1032是 MySQL 主从复制时的经典错误之一&#xff0c;通常表示&#xff…...

十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建

【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...

Python训练营-Day26-函数专题1:函数定义与参数

题目1&#xff1a;计算圆的面积 任务&#xff1a; 编写一个名为 calculate_circle_area 的函数&#xff0c;该函数接收圆的半径 radius 作为参数&#xff0c;并返回圆的面积。圆的面积 π * radius (可以使用 math.pi 作为 π 的值)要求&#xff1a;函数接收一个位置参数 radi…...

32单片机——基本定时器

STM32F103有众多的定时器&#xff0c;其中包括2个基本定时器&#xff08;TIM6和TIM7&#xff09;、4个通用定时器&#xff08;TIM2~TIM5&#xff09;、2个高级控制定时器&#xff08;TIM1和TIM8&#xff09;&#xff0c;这些定时器彼此完全独立&#xff0c;不共享任何资源 1、定…...