前言
基本上每個系統系統都包含使用者註冊、傳送驗證碼等基本操作。在前些年,我還記得我在逛 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. 生成驗證碼唯一key
String 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);
// 判斷驗證碼code
if (!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);
}
// 生成郵箱驗證碼code
String emailCode = RandomUtil.randomString(6);
// 生成郵箱驗證碼唯一key
String 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】每週分享技術乾貨、開源專案、實戰經驗、國外優質文章翻譯等,您的關注將是我的更新動力!