Flask-爱家租房项目ihome-02-注册

图片验证码功能

导入验证码生成逻辑

utils中导入验证码逻辑,fonts文件夹和captcha.py文件

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# refer to `https://bitbucket.org/akorn/wheezy.captcha`
import random
import string
import os.path
# from cStringIO import StringIO py2.x
# from io import StringIO # py3.x
from io import BytesIO  # py3.x

from PIL import Image
from PIL import ImageFilter
from PIL.ImageDraw import Draw
from PIL.ImageFont import truetype

class Bezier:
    def __init__(self):
        self.tsequence = tuple([t / 20.0 for t in range(21)])
        self.beziers = {}

    def pascal_row(self, n):
        """ Returns n-th row of Pascal's triangle
        """
        result = [1]
        x, numerator = 1, n
        for denominator in range(1, n // 2 + 1):
            x *= numerator
            x /= denominator
            result.append(x)
            numerator -= 1
        if n & 1 == 0:
            result.extend(reversed(result[:-1]))
        else:
            result.extend(reversed(result))
        return result

    def make_bezier(self, n):
        """ Bezier curves:
            http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Generalization
        """
        try:
            return self.beziers[n]
        except KeyError:
            combinations = self.pascal_row(n - 1)
            result = []
            for t in self.tsequence:
                tpowers = (t ** i for i in range(n))
                upowers = ((1 - t) ** i for i in range(n - 1, -1, -1))
                coefs = [c * a * b for c, a, b in zip(combinations,
                                                      tpowers, upowers)]
                result.append(coefs)
            self.beziers[n] = result
            return result

class Captcha(object):
    def __init__(self):
        self._bezier = Bezier()
        self._dir = os.path.dirname(__file__)
        # self._captcha_path = os.path.join(self._dir, '..', 'static', 'captcha')

    @staticmethod
    def instance():
        if not hasattr(Captcha, "_instance"):
            Captcha._instance = Captcha()
        return Captcha._instance

    def initialize(self, width=200, height=75, color=None, text=None, fonts=None):
        # self.image = Image.new('RGB', (width, height), (255, 255, 255))
        # self._text = text if text else random.sample(string.uppercase + string.uppercase + '3456789', 4)
        self._text = text if text else random.sample(string.ascii_uppercase +
                                                     string.ascii_uppercase +
                                                     '3456789', 4)  # py3.x
        self.fonts = fonts if fonts else \
            [os.path.join(self._dir, 'fonts', font) for font in ['Arial.ttf', 'Georgia.ttf', 'actionj.ttf']]
        self.width = width
        self.height = height
        self._color = color if color else self.random_color(0, 200, random.randint(220, 255))

    @staticmethod
    def random_color(start, end, opacity=None):
        red = random.randint(start, end)
        green = random.randint(start, end)
        blue = random.randint(start, end)
        if opacity is None:
            return red, green, blue
        return red, green, blue, opacity

    # draw image

    def background(self, image):
        Draw(image).rectangle([(0, 0), image.size], fill=self.random_color(238, 255))
        return image

    @staticmethod
    def smooth(image):
        return image.filter(ImageFilter.SMOOTH)

    def curve(self, image, width=4, number=6, color=None):
        dx, height = image.size
        dx /= number
        path = [(dx * i, random.randint(0, height))
                # for i in xrange(1, number)]
                for i in range(1, number)]  # py3.x
        bcoefs = self._bezier.make_bezier(number - 1)
        points = []
        for coefs in bcoefs:
            points.append(tuple(sum([coef * p for coef, p in zip(coefs, ps)])
                                for ps in zip(*path)))
        Draw(image).line(points, fill=color if color else self._color, width=width)
        return image

    def noise(self, image, number=50, level=2, color=None):
        width, height = image.size
        dx = width / 10
        width -= dx
        dy = height / 10
        height -= dy
        draw = Draw(image)
        # for i in xrange(number):
        for i in range(number):  # py3.x
            x = int(random.uniform(dx, width))
            y = int(random.uniform(dy, height))
            draw.line(((x, y), (x + level, y)), fill=color if color else self._color, width=level)
        return image

    def text(self, image, fonts, font_sizes=None, drawings=None, squeeze_factor=0.75, color=None):
        color = color if color else self._color
        fonts = tuple([truetype(name, size)
                       for name in fonts
                       for size in font_sizes or (65, 70, 75)])
        draw = Draw(image)
        char_images = []
        for c in self._text:
            font = random.choice(fonts)
            c_width, c_height = draw.textsize(c, font=font)
            char_image = Image.new('RGB', (c_width, c_height), (0, 0, 0))
            char_draw = Draw(char_image)
            char_draw.text((0, 0), c, font=font, fill=color)
            char_image = char_image.crop(char_image.getbbox())
            for drawing in drawings:
                d = getattr(self, drawing)
                char_image = d(char_image)
            char_images.append(char_image)
        width, height = image.size
        offset = int((width - sum(int(i.size[0] * squeeze_factor)
                                  for i in char_images[:-1]) -
                      char_images[-1].size[0]) / 2)
        for char_image in char_images:
            c_width, c_height = char_image.size
            mask = char_image.convert('L').point(lambda i: i * 1.97)
            image.paste(char_image,
                        (offset, int((height - c_height) / 2)),
                        mask)
            offset += int(c_width * squeeze_factor)
        return image

    # draw text
    @staticmethod
    def warp(image, dx_factor=0.27, dy_factor=0.21):
        width, height = image.size
        dx = width * dx_factor
        dy = height * dy_factor
        x1 = int(random.uniform(-dx, dx))
        y1 = int(random.uniform(-dy, dy))
        x2 = int(random.uniform(-dx, dx))
        y2 = int(random.uniform(-dy, dy))
        image2 = Image.new('RGB',
                           (width + abs(x1) + abs(x2),
                            height + abs(y1) + abs(y2)))
        image2.paste(image, (abs(x1), abs(y1)))
        width2, height2 = image2.size
        return image2.transform(
            (width, height), Image.QUAD,
            (x1, y1,
             -x1, height2 - y2,
             width2 + x2, height2 + y2,
             width2 - x2, -y1))

    @staticmethod
    def offset(image, dx_factor=0.1, dy_factor=0.2):
        width, height = image.size
        dx = int(random.random() * width * dx_factor)
        dy = int(random.random() * height * dy_factor)
        image2 = Image.new('RGB', (width + dx, height + dy))
        image2.paste(image, (dx, dy))
        return image2

    @staticmethod
    def rotate(image, angle=25):
        return image.rotate(
            random.uniform(-angle, angle), Image.BILINEAR, expand=1)

    def captcha(self, path=None, fmt='JPEG'):
        """Create a captcha.

        Args:
            path: save path, default None.
            fmt: image format, PNG / JPEG.
        Returns:
            A tuple, (name, text, StringIO.value).
            For example:
                ('fXZJN4AFxHGoU5mIlcsdOypa', 'JGW9', '\x89PNG\r\n\x1a\n\x00\x00\x00\r...')

        """
        image = Image.new('RGB', (self.width, self.height), (255, 255, 255))
        image = self.background(image)
        image = self.text(image, self.fonts, drawings=['warp', 'rotate', 'offset'])
        image = self.curve(image)
        image = self.noise(image)
        image = self.smooth(image)
        # name = "".join(random.sample(string.lowercase + string.uppercase + '3456789', 24))
        name = "".join(random.sample(string.ascii_lowercase + string.ascii_uppercase + '3456789', 24))  # py3.x
        text = "".join(self._text)
        # out = StringIO()# py2.x
        out = BytesIO()  # py3.x
        image.save(out, format=fmt)
        if path:
            image.save(os.path.join(path, name), fmt)
        return name, text, out.getvalue()

    def generate_captcha(self):
        self.initialize()
        return self.captcha("")

captcha = Captcha.instance()

if __name__ == '__main__':
    print(captcha.generate_captcha())

编写图片验证码接口

逻辑梳理:

  1. 将获取图片验证码的功能做成一个独立的接口,浏览器访问url:http://127.0.0.1:5000/api/v1.0/image_codes/<image_code_key>即可获得一张验证码图片,其中url需要携带一个image_code_key参数,用来标记是谁获取的图片验证码
  2. 浏览器访问注册页面或者点击验证码图片时,就会调用这个验证码图片接口,传入一个全局唯一值image_code_key
  3. 接口中拿到image_code_key后,调用上面的图片验证码生成逻辑,获取到验证码图片image_data和真实的string验证码text,将键值对{image_code_key:text}存入redis数据库中,并设置过期时间,再将image_data返回给前端.
  4. 前端展示返回的验证码图片给用户识别并让用户填写验证码,点击获取验证码按钮时,调用验证验证码是否输入成功的逻辑.

ihome/api_1_0目录中添加验证码模块verify_code.py

# /ihome/api_1_0/verify_codes.py
from flask import make_response, jsonify, current_app
from ihome.utils.captcha import captcha
from ihome.utils.constants import IMAGE_CODE_REDIS_EXPIRES
from ihome.utils.response_codes import RET
from . import api
from ihome import redis_connect

@api.route('/image_codes/<image_code_key>')
def get_image_code(image_code_key):
    # 获取验证码
    name, text, image_data = captcha.generate_captcha()
    print(f'真实验证码:{text}')
    # 保存验证码
    try:
        redis_connect.setex(image_code_key, IMAGE_CODE_REDIS_EXPIRES, text)  # setex(key, 过期时间, value)=set+expire
    except Exception as e:
        # 保存失败, 记录日志
        current_app.logger.error(e)
        return jsonify(error_code=RET.DBERR, error_msg='保存图片验证码失败')
    # 保存成功
    resp = make_response(image_data)
    resp.headers["Content-Type"] = "image/jpg"
    return resp

在上述代码中导入了自定义的常量模块ihome.utils.constants和状态码模块ihome.utils.response_codes,比较好集中管理常量和与前端约定的返回状态码

# ihome.utils.constants.py

# 图片验证码的redis有效期, 单位:秒
IMAGE_CODE_REDIS_EXPIRES = 180  
# 短信验证码的redis有效期, 单位:秒
SMS_CODE_REDIS_EXPIRES = 300
# 发送短信验证码的间隔, 单位:秒
SEND_SMS_CODE_INTERVAL = 60
# ihome.utils.response_codes

class RET:
    OK = "0"             # "成功"
    DBERR = "4001"       # "数据库查询错误"
    NODATA = "4002"      # "无数据"
    DATAEXIST = "4003"   # "数据已存在"
    DATAERR = "4004"     # "数据错误"
    SESSIONERR = "4101"  # "用户未登录"
    LOGINERR = "4102"    # "用户登录失败"
    PARAMERR = "4103"    # "参数错误"
    USERERR = "4104"     # "用户不存在或未激活"
    ROLEERR = "4105"     # "用户身份错误"
    PWDERR = "4106"      # "密码错误"
    REQERR = "4201"      # "非法请求或请求次数受限"
    IPERR = "4202"       # "IP受限"
    THIRDERR = "4301"    # "第三方系统错误"
    IOERR = "4302"       # "文件读写错误"
    SERVERERR = "4500"   # "内部错误"
    UNKOWNERR = "4501"   # "未知错误"

浏览器访问接口http://127.0.0.1:5000/api/v1.0/image_codes/123测试结果

编写前端获取图片验证码逻辑

// ihome/static/js/ihome/register.js
// 进入注册页面自动调用generateImageCode(),获取验证码图片
$(document).ready(function() {
    generateImageCode();
    ......

// 形成图片验证码的后端地址, 设置到页面中,让浏览请求验证码图片
function generateImageCode() {
    // 1. 生成图片验证码编号
    imageCodeId = generateUUID();
    // 2. 设置图片url
    var url = "/api/v1.0/image_codes/" + imageCodeId;
    $(".image-code img").attr("src", url);
}

// 保存图片验证码编号
var imageCodeId = "";
// 生成全局唯一值的ID
function generateUUID() {
    var d = new Date().getTime();
    if(window.performance && typeof window.performance.now === "function"){
        d += performance.now(); //use high-precision timer if available
    }
    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = (d + Math.random()*16)%16 | 0;
        d = Math.floor(d/16);
        return (c=='x' ? r : (r&0x3|0x8)).toString(16);
    });
    return uuid;
}

短信验证码功能

使用第三方平台容联云通讯提供的短信验证码功能,这里只测试使用短信验证码的功能, 因此只需完成注册登录(无需实名认证等)即可使用其短信验证码免费测试服务, 不过免费测试服务只能给控制台中指定的三个手机号发送短信, 且只能有一个短信模板可以使用.

安装容联云短信发送模块

pip install ronglian_sms_sdk

编辑短信发送接口

编辑verify_codes.py,添加简单的短信发送逻辑,测试是否能成功发送短信

# /ihome/api_1_0/verify_codes.py
from ronglian_sms_sdk import SmsSDK
from . import api
import random

@api.route('/sms_codes/<phone>')
def send_sms_code(phone):
    # 创建sdk对象,三个参数为容联云官网账号下的ID,这里设置在了constants.py中
    sdk = SmsSDK(constants.ACCID, constants.ACCTOKEN, constants.APPID)
    # 短信模板编号,该模板为:....您的验证码是{1},请于{2}分钟内正确输入
    tid = '1'
    # 接受短信的手机号,在测试环境中这个手机号需要在容联云的测试号码中维护
    mobile = phone
    # 获取随机6位验证码,容联云不会生成验证码,需要我们自己生成
    sms_code = '%06d' % random.randint(0, 999999)
    # data参数为二元元组,第一个值为模板中的{1},第二个值为模板中的{2}
    data = (sms_code, constants.SMS_CODE_REDIS_EXPIRES//60)
    # 调用发送短信的方法,返回的时json字符串
    resp = sdk.sendMessage(tid, mobile, data)
    print(f'resp: {resp}')
    return 'ok'

浏览器访问http://127.0.0.1:5000/api/v1.0/sms_codes/17621081762

可以正常接收到短信并且打印出的日志为:

Sign plaintext:  8aaf07087249953401727fa88e1e1c9d653e8d1fe37d40b589ad85948fefc3ce20200815185159
Authorization plaintext: 8aaf07087249953401727fa88e1e1c9d:20200815185159
Request url:  https://app.cloopen.com:8883/2013-12-26/Accounts/8aaf07087249953401727fa88e1e1c9d/SMS/TemplateSMS?sig=0F271A07ED79DF36628DB3DDA2D08F0A
Request headers:  {'Content-Type': 'application/json;charset=utf-8', 'Accept': 'application/json', 'Accept-Charset': 'UTF-8', 'Authorization': b'OGFhZjA3MDg3MjQ5OTUzNDAxNzI3ZmE4OGUxZTFjOWQ6MjAyMDA4MTUxODUxNTk='}
Request body:  {"to": "17621081762", "appId": "8aaf07087249953401727fa88f011ca4", "templateId": "1", "datas": ["936497", 5]}
Response body:  {"statusCode":"000000","templateSMS":{"smsMessageSid":"2e41f82f7adc42e992431cd11637f94d","dateCreated":"20200815185200"}}
resp: {"statusCode":"000000","templateSMS":{"smsMessageSid":"2e41f82f7adc42e992431cd11637f94d","dateCreated":"20200815185200"}}

完善短信接口功能

  1. 给url中手机号参数phone添加正则转换器

  2. 添加验证图片验证码的逻辑

  3. 添加同一手机号一分钟内不能多次发送短信逻辑

  4. redis中保存短信验证码,给注册接口使用

@api.route("/sms_codes/<re(r'1[^120]\d{9}'):phone>")
def send_sms_code(phone):
    # 获取url中?参数
    image_code = request.args.get('image_code')
    image_code_key = request.args.get('image_code_key')
    # 判断参数是否有值
    if not all([image_code, image_code_key]):
        return jsonify(errno=RET.PARAMERR, errmsg='图片验证码参数不能为空')

    # 判断该手机号是否已经注册过
    try:
        user = Users.query.filter_by(phone=phone).first()
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='获取用户异常')
    if user:
        return jsonify(errno=RET.DATAEXIST, errmsg='该手机号已注册过用户')

    # 判断该手机号一分钟内是否发送过短信
    try:
        his_sms_code_key = f'his_sms_code_{phone}'
        sms_his = redis_connect.get(his_sms_code_key)
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='redis获取短信验证码异常')
    # 存在短信记录
    if sms_his:
        return jsonify(errno=RET.DATAEXIST, errmsg='该手机号一分钟内已发送过短信,请稍候再试')

    # 获取redis中的真实图片验证码
    try:
        real_image_code = redis_connect.get(image_code_key)
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='读取图片验证码异常')
    # 判断验证码是否存在
    if not real_image_code:
        return jsonify(errno=RET.NODATA, errmsg='图片验证码已失效')
    # 删除redis中的图片验证码,防止同一个验证码被使用多次
    try:
        redis_connect.delete(image_code_key)
    except Exception as e:
        current_app.logger.error(e)
    # 比较用户输入的验证码与真实验证码
    if image_code.lower() != real_image_code.decode().lower():
        # 验证码错误
        return jsonify(errno=RET.DATAERR, errmsg='图片验证码错误')

    # 图片验证码正确,则发送短信验证码
    # 获取随机6位验证码
    # sms_code = '%06d' % random.randint(0, 999999)
    sms_code = f'{random.randint(0,999999):06}'
    try:
        sdk = SmsSDK(constants.ACCID, constants.ACCTOKEN, constants.APPID)
        tid = '1'
        mobile = phone
        # 短信模板为:....您的验证码是{1},请于{2}分钟内正确输入
        # data参数为元组,第一个值为模板中的{1},第二个值为模板中的{2}
        data = (sms_code, constants.SMS_CODE_REDIS_EXPIRES//60)
        # 发送短信,接受返回值
        sms_resp_json = sdk.sendMessage(tid, mobile, data)
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.THIRDERR, errmsg='发送短信异常')
    # 处理返回值
    sms_resp_dict = json.loads(sms_resp_json)
    sms_status = sms_resp_dict.get('statusCode')
    if sms_status != '000000':
        # 发送失败
        return jsonify(errno=RET.THIRDERR, errmsg=sms_resp_dict.get('statusMsg'))

    # 发送成功
    # redis缓存发送记录,一分钟内同一个手机号不能再发送短信了
    try:
        redis_connect.setex(his_sms_code_key, constants.SEND_SMS_CODE_INTERVAL, sms_code)
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='redis保存短信验证码记录异常')

    # 保存短信验证码
    try:
        sms_code_key = f'sms_code_{phone}'
        redis_connect.setex(sms_code_key, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='redis保存短信验证码异常')

    return jsonify(errno=RET.OK)

编写前端短信验证码逻辑

给注册页面获取验证码按钮添加点击事件sendSMSCode

// register.py
function sendSMSCode() {
    // 点击一次之后移除点击功能,防止重复点击
    $(".phonecode-a").removeAttr("onclick");
    // 校验手机号和图片验证码是否填写
    var mobile = $("#mobile").val();
    if (!mobile) {
        $("#mobile-err span").html("请填写正确的手机号!");
        $("#mobile-err").show();
        $(".phonecode-a").attr("onclick", "sendSMSCode();");
        return;
    }
    var imageCode = $("#imagecode").val();
    if (!imageCode) {
        $("#image-code-err span").html("请填写验证码!");
        $("#image-code-err").show();
        $(".phonecode-a").attr("onclick", "sendSMSCode();");
        return;
    }
    // ajax调用后台接口
    $.get("/api/v1.0/sms_codes/" + mobile, {image_code:imageCode, image_code_key:imageCodeId},
        function(data){
            if (0 != data.errno) {
                // 后台返回错误,则展示错误
                $("#image-code-err span").html(data.errmsg);
                $("#image-code-err").show();
                if (2 == data.errno || 3 == data.errno) {
                    generateImageCode();
                }
                // 重新设置点击功能
                $(".phonecode-a").attr("onclick", "sendSMSCode();");
            }
            else {
                // 短信发送成功
                var $time = $(".phonecode-a");
                var duration = 60;
                // 设置计时器,60秒内不允许再次点击
                var intervalid = setInterval(function(){
                    $time.html(duration + "秒");
                    if(duration === 1){
                        clearInterval(intervalid);
                        $time.html('获取验证码');
                        $(".phonecode-a").attr("onclick", "sendSMSCode();");
                    }
                    duration = duration - 1;
                }, 1000, 60);
            }
    }, 'json');
}

注册接口逻辑

ihome/api_1_0下新增用户模块文件users.py,增加用户注册逻辑,并在蓝图中导入该视图文件

from flask import request, jsonify, current_app, session
from sqlalchemy.exc import IntegrityError
from . import api
from ihome import models, csrf, redis_connect, db
from ihome.utils.response_codes import RET
from ihome.models import Users
import re
import json

# @csrf.exempt 该装饰器为取消csrf防护
@api.route('/users', methods=['POST'])
def register():
    # 获取数据
    dict_data = request.get_json()
    phone = dict_data.get('mobile')
    sms_code = dict_data.get('phoneCode')
    password = dict_data.get('password')
    password2 = dict_data.get('password2')

    # 校验数据
    if not all([phone, sms_code, password, password2]):
        return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')

    # 两次密码是否一致
    if password2 != password:
        return jsonify(errno=RET.PARAMERR, errmsg='两次密码不一致')

    # 校验手机号
    if not re.match(r'1[^120]\d{9}', phone):
        return jsonify(errno=RET.PARAMERR, errmsg='手机号格式不正确')

    # 验证短信验证码
    sms_code_key = f'sms_code_{phone}'
    # 获取真实短信验证码
    try:
        real_sms_code = redis_connect.get(sms_code_key)
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='获取redis验证码异常')
    # 校验验证码
    if not real_sms_code:
        return jsonify(errno=RET.NODATA, errmsg='验证码不存在或已过期')
    if sms_code != real_sms_code.decode():
        return jsonify(errno=RET.PARAMERR, errmsg='验证码不正确')

    # 创建用户
    user = Users(phone=phone, password=password, name=phone)
    try:
        db.session.add(user)
        db.session.commit()
    except IntegrityError as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DATAEXIST, errmsg='该手机号已注册过用户')
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='创建用户异常')

    # 注册则认为已经登录, 设置session,记录登录状态
    session['user_id'] = user.id
    session['phone'] = phone
    session['name'] = phone

    return jsonify(errno=RET.OK)

注:

  1. @csrf.exempt 该装饰器可以取消视图函数的csrf防护,导入的csrf为创建app时的csrf对象csrf = CSRFProtect(app),这里先不取消防护
  2. flask的request.get_json()可以直接获取到前端传入的json数据,返回值类型为json字符串
  3. 校验该手机号是否已经创建过用户可以利用Users表中phone字段的唯一性约束特性,若已注册过用户,那么再次以该手机号创建用户时,会抛出sqlalchemy.exc.IntegrityError异常,处理该异常即可
  4. 注册成功则认为已经登录, 设置session

注册的前端逻辑

由于该项目前后端使用json传输,所以不能使用表单自带的提交功能,需要我们自定义表单的提交和后续的处理,在register.js中添加该部分逻辑

$(document).ready(function() {
    generateImageCode();
    ......
    // 为表单的提交补充自定义的函数行为,参数e为提交事件
    $(".form-register").submit(function(e){
        //阻止默认的提交表单行为,走下面自定义的行为
        e.preventDefault();
        //判断字段是否填写
        var mobile = $("#mobile").val();
        var phoneCode = $("#phonecode").val();
        var passwd = $("#password").val();
        var passwd2 = $("#password2").val();
        if (!mobile) {
            $("#mobile-err span").html("请填写正确的手机号!");
            $("#mobile-err").show();
            return;
        }
        if (!phoneCode) {
            $("#phone-code-err span").html("请填写短信验证码!");
            $("#phone-code-err").show();
            return;
        }
        if (!passwd) {
            $("#password-err span").html("请填写密码!");
            $("#password-err").show();
            return;
        }
        if (passwd != passwd2) {
            $("#password2-err span").html("两次密码不一致!");
            $("#password2-err").show();
            return;
        }
        //发送ajax请求
        //js对象
        var postData = {
            mobile: mobile,
            phoneCode: phoneCode,
            password: passwd,
            password2: passwd2
        };
        //转换为json格式
        var postJson = JSON.stringify(postData);
        $.ajax({
            url: "api/v1.0/users",
            type: "post",
            contentType: "application/json",
            data: postJson,
            dataType: "json",
            headers: {'X-CSRFToken': getCookie('csrf_token')},
            success: function (resp) {
                if(resp.errno != '0'){
                    alert(resp.errmsg);
                }
                else{
                    //注册成功,跳转到首页
                    location.href='/index.html';
                }
            }
        })
    });
})

注:

  1. 在注册页面一加载就阻止默认的提交表单行为e.preventDefault();

  2. 使用JSON.stringify()方法将js对象格式转化为json格式

  3. 因为需要设置请求头等比较多的信息,所以使用ajax的完整写法,而不是用$.post简写

  4. ajax设置发送和接受的数据都是json格式的

  5. 由于后端添加了csrf防护,所以前端需要发送csrf_token,又因为如果在请求体中发送csrf_token的话,必须要请求体的格式为form表单格式,而我们这里是使用的json格式,所以只能在请求头中发送csrf_token,因此在请求头中添加X-CSRFToken属性

  6. X-CSRFToken的值就是之前设置在浏览器cookie中的csrf_token的值,因此需要获取到cookie中的值,这里的getCookie()方法就是用来获取cookie的,原理就是document.cookie获取所有的cookie,再通过正则表达式提取csrf_token的值

    function getCookie(name) {
        var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
        return r ? r[1] : undefined;
    }
    
  7. document.cookie.match()的输出为一个列表,第0个位置为完整的cookie信息,第一个位置为cookie值

    >document.cookie.match("\\b" + 'csrf_token' + "=([^;]*)\\b");
    <(2) ["csrf_token=IjE4ZmU1ZGNkNzNkODU0NDA0YTMzMDQyYmYxNzI…A3NzI4ZTM2YzAi.XztX7g.E_9RCL50tFgogPM9AtAH54KXxZU", "IjE4ZmU1ZGNkNzNkODU0NDA0YTMzMDQyYmYxNzI2NzA3NzI4ZTM2YzAi.XztX7g.E_9RCL50tFgogPM9AtAH54KXxZU", index: 0, input: "csrf_token=IjE4ZmU1ZGNkNzNkODU0NDA0YTMzMDQyYmYxNzI…A3NzI4ZTM2YzAi.XztX7g.E_9RCL50tFgogPM9AtAH54KXxZU", groups: undefined]
    
posted @ 2020-08-19 10:13  Alex-GCX  阅读(336)  评论(0编辑  收藏  举报