第三章:注册功能实现——表单验证、密码强度检测、邮箱/手机号唯一性检查

好,咱们接着聊注册功能。说实话,注册是用户进入系统的第一道门,也是我踩坑最多的地方。你想想看,一个注册功能要是做得不严谨,后面所有权限管理都是白搭。我个人习惯把注册拆成三个核心环节:表单验证密码强度检测唯一性检查。这三个环节缺一不可,顺序也不能乱。

3.1 表单验证:别让脏数据进门

表单验证,说白了就是检查用户填的东西合不合规矩。我在项目中遇到过最离谱的事——用户把手机号填成了「12345678901」,少了一位,结果系统还让他注册成功了。后来查日志才发现,前端验证没做,后端也没兜底。

嗯,这里要注意:前端验证是用户体验,后端验证才是安全底线。前端可以帮你过滤掉90%的明显错误,但后端必须做100%的校验。我建议你至少检查这几项:

  • 必填项:用户名、密码、邮箱/手机号,一个都不能少
  • 格式校验:邮箱要有@和域名,手机号要符合位数规则
  • 长度限制:用户名别超过50字,密码别少于8位
  • 特殊字符过滤:防止XSS和SQL注入

核心原则:永远不要信任用户输入。哪怕用户是你亲妈,也要做验证。

来看一段后端验证的代码示例,我用的是Flask + WTForms:

from wtforms import Form, StringField, PasswordField, validators

class RegistrationForm(Form):
    username = StringField('用户名', [
        validators.DataRequired(message='用户名不能为空'),
        validators.Length(min=2, max=20, message='用户名长度2-20位')
    ])
    email = StringField('邮箱', [
        validators.DataRequired(message='邮箱不能为空'),
        validators.Email(message='请输入有效的邮箱地址')
    ])
    phone = StringField('手机号', [
        validators.DataRequired(message='手机号不能为空'),
        validators.Regexp(r'^1[3-9]\d{9}$', message='手机号格式不正确')
    ])
    password = PasswordField('密码', [
        validators.DataRequired(message='密码不能为空'),
        validators.Length(min=8, max=128, message='密码长度8-128位')
    ])

这段代码里,我用了正则表达式来校验手机号。为什么是^1[3-9]\d{9}$?因为目前国内手机号第二位从3到9都有,11位数字。你想想看,要是用^1\d{10}$,那「10000000000」这种假号也能过,对吧?

3.2 密码强度检测:别让用户用「123456」

密码强度检测,这个我太有发言权了。我曾经维护过一个老系统,用户密码全是「123456」或者「password」。后来被脱库了,老板让我背锅。从那以后,我对密码强度检测就特别较真。

我个人习惯把密码强度分成三个等级:

等级 要求 示例
纯数字或纯字母,长度不足8位 123456, abcdef
数字+字母混合,长度8-12位 abc12345
数字+大小写字母+特殊字符,长度12位以上 Abc@2024#Secure

我建议注册时至少要求「中」等级,最好强制「强」等级。为什么?因为弱密码在暴力破解面前,撑不过1秒。你想想看,现在GPU跑字典的速度有多快?

来看一个密码强度检测的Python实现:

import re

def check_password_strength(password):
    score = 0
    if len(password) >= 8:
        score += 1
    if len(password) >= 12:
        score += 1
    if re.search(r'[a-z]', password):
        score += 1
    if re.search(r'[A-Z]', password):
        score += 1
    if re.search(r'\d', password):
        score += 1
    if re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        score += 1
    
    if score <= 2:
        return '弱'
    elif score <= 4:
        return '中'
    else:
        return '强'

小技巧:前端也可以做实时强度检测,用颜色条显示。绿色代表强,黄色代表中,红色代表弱。用户看到红色,自己就会改密码。

这里有个坑——不要存储明文密码。我曾经见过一个项目,密码直接存数据库里,连MD5都没做。后来被脱库,用户信息全裸奔。正确的做法是用bcrypt或Argon2做哈希,加盐处理。

3.3 邮箱/手机号唯一性检查:别让用户重复注册

唯一性检查,说白了就是确保同一个邮箱或手机号只能注册一个账号。这个逻辑看起来简单,但实现起来有讲究。

我建议分两步走:

  1. 注册前检查:用户填完邮箱/手机号,点击「获取验证码」时,先查一下数据库里有没有这个记录。如果有,直接提示「该邮箱/手机号已被注册」。
  2. 注册时检查:提交表单时,再次检查唯一性。为什么?因为用户可能在输入验证码的这段时间里,另一个用户已经注册了同一个邮箱。

注意:唯一性检查一定要加数据库唯一索引。光靠代码检查是不够的,高并发下会出现「竞态条件」。我曾经就因为这个,让两个用户注册了同一个手机号,后来对账对了一整天。

来看数据库设计:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    phone VARCHAR(20) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

注意看,emailphone都加了UNIQUE约束。这是最后一道防线,哪怕代码有bug,数据库也能兜住。

后端检查逻辑:

def check_uniqueness(email, phone):
    existing_user = User.query.filter(
        (User.email == email) | (User.phone == phone)
    ).first()
    
    if existing_user:
        if existing_user.email == email:
            return {'error': '该邮箱已被注册'}
        if existing_user.phone == phone:
            return {'error': '该手机号已被注册'}
    return None

这里我用了OR条件,一次查询搞定。为什么不用两次查询?因为数据库连接是宝贵的资源,能省则省。你想想看,如果每个注册请求都查两次数据库,并发一上来,数据库就扛不住了。

3.4 整合:完整的注册流程

好了,三个环节都讲完了。咱们把它们串起来,看看完整的注册流程是什么样的:

  1. 用户填写注册表单
  2. 前端做基本格式验证(比如邮箱格式、密码长度)
  3. 用户点击「获取验证码」,后端检查邮箱/手机号唯一性
  4. 用户输入验证码,提交表单
  5. 后端做完整表单验证(包括密码强度检测)
  6. 后端再次检查唯一性(防止并发问题)
  7. 验证通过,密码哈希存储,创建用户记录
  8. 返回注册成功消息

记住:注册功能是用户信任你的第一步。做得好了,用户觉得你专业;做得差了,用户转身就走。我见过太多项目,注册体验一塌糊涂,结果用户流失率高达60%。

最后说一句,验证码也是注册功能的重要一环。虽然这节课没展开讲,但你要知道,验证码能有效防止机器注册。我个人习惯用短信验证码+图形验证码双重保险。嗯,这个咱们后面章节再细聊。

好,注册功能就讲到这里。下一章咱们聊聊登录功能——Session管理、JWT令牌、记住我功能。到时候见。