欢迎来到十九分快乐的博客

生死看淡,不服就干。

5. 用户登陆

用户登录

jwt登陆认证

# 头部 header
hhhhhhhhhhhhhhhhhhhhhhhh = base64.b64encode(header)
{
	"typ": "JWT", # 类型 
	"alg": "H256" # 加密方式
}

# 载荷 preload - 用户信息
pppppppppppppppppppppppp = base64.b64encode(preload)
{
   # 标准声明,
   "expire":"xxxx",
   "server":"",
   # 公共声明
   "user_id":"1",
   "user_status": true,
   # 私有声明
   "xxxx": "xxxxxxx",
}

# 签证 sign 用于防止客户端中的jwt被人恶意串改的
xxxxxxxxxxxxxxxxxxxxxxxx = base64.b64encode( H256(头部.载荷.秘钥) )

hhhhhhhhhhhhhhhhhhhhhhhh.pppppppppppppppppppppppp.xxxxxxxxxxxxxxxxxxxxxxxx


jwt 一般通过三种方式发送给服务端:
1. query String 查询字符串
2. request head 请求头[荏苒项目中使用的这个] 
3. request body 请求体

当前我们开发的项目属于前后端分离,而目前最适合我们使用的认证方式就是jwt token认证。也有些公司采用 oauth token认证

在flask中,我们可以通过flask_jwt_extended模块来快速实现jwt用户登录认证。

注意:

  1. flask_jwt_extended的作者开发当前模块主要适用于flask的普通视图方法的。其认证方式主要通过装饰器来完成。而我们当前所有服务端接口都改造成了jsonrpc规范接口,所以我们在使用过程中,需要对部分源代码进行调整才能正常使用
  2. 事实上,在我们当前使用的flask_jsonrpc也提供了基于用户名username和密码password进行的用户登陆认证功能,但是这个功能是依靠用户账户username和密码password来实现。如果我们基于当前这种方式,也可以实现jwt登陆认证,只是相对于上面的flask_jwt_extended模块而言,要补充的代码会更多,所以在此,我们放弃这块功能的使用。

模块安装

pip install flask-jwt-extended  -i https://pypi.douban.com/simple

官网文档:https://flask-jwt-extended.readthedocs.io/en/latest/

配置说明:https://flask-jwt-extended.readthedocs.io/en/latest/options/

初始化配置

  1. 在魔方APP项目中对模块进行初始化,application/__init__.py,代码:
import os

from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from flask_session import Session
from flask_jsonrpc import JSONRPC
from faker import Faker
from celery import Celery
from flask_jwt_extended import JWTManager

from application.utils.config import init_config
from application.utils.logger import Log
from application.utils.commands import load_commands
from application.utils.bluerprint import register_blueprint, path, include, api_rpc
from application.utils import message, code
from application.utils.unittest import BasicTestCase

# 终端脚本工具初始化
manager = Manager()

# SQLAlchemy初始化
db = SQLAlchemy()

# redis数据库初始化
# - 1.默认缓存数据库对象,配置前缀为REDIS
redis_cache = FlaskRedis(config_prefix='REDIS')
# - 2.验证相关数据库对象,配置前缀为CHECK
redis_check = FlaskRedis(config_prefix='CHECK')
# - 3.验证相关数据库对象,配置前缀为SESSION
redis_session = FlaskRedis(config_prefix='SESSION')

# session储存配置初始化
session_store = Session()

# 自定义日志初始化
logger = Log()

# 初始化jsonrpc模块
jsonrpc = JSONRPC()

# 初始化随机生成数据模块faker
faker = Faker(locale='zh-CN') # 指定中文

# 初始化异步celery
celery = Celery()

# jwt认证模块初始化
jwt = JWTManager()

# 全局初始化
def init_app(config_path):
    """全局初始化 - 需要传入加载开发或生产环境配置路径"""
    # 创建app应用对象
    app = Flask(__name__)

    # 当前项目根目录
    app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    # 开发或生产环境加载配置
    init_config(app, config_path)

    # SQLAlchemy加载配置
    db.init_app(app)

    # redis加载配置
    redis_cache.init_app(app)
    redis_check.init_app(app)
    redis_session.init_app(app)

    """一定先加载默认配置,再传入APP加载session对象"""
    # session保存数据到redis时启用的链接对象
    app.config["SESSION_REDIS"] = redis_session
    # session存储对象加载配置
    session_store.init_app(app)

    # 为日志对象加载配置
    log = logger.init_app(app)
    app.log = log

    # json-rpc加载配置
    jsonrpc.init_app(app)
    # rpc访问路径入口(只有唯一一个访问路径入口),默认/api
    jsonrpc.service_url = app.config.get('JSON_SERVER_URL', '/api')
    jsonrpc.enable_web_browsable_api = app.config.get("ENABLE_WEB_BROWSABLE_API",False)
    app.jsonrpc = jsonrpc

    # 自动注册蓝图
    register_blueprint(app)

    # 加载celery配置
    celery.main = app.name
    celery.app = app
    # 更新配置
    celery.conf.update(app.config)
    # 自动注册任务
    celery.autodiscover_tasks(app.config.get('INSTALL_BLUEPRINT'))

    # jwt认证加载配置
    jwt.init_app(app)

    # 注册模型,创建表
    with app.app_context():
        db.create_all()

    # 终端脚本工具加载配置
    manager.app = app

    # 自动注册自定义命令
    load_commands(manager)

    return manager
  1. 开发环境配置,application/settings/dev.py,代码:
"""jwt认证配置"""
# 加密算法,默认: HS256
JWT_ALGORITHM = "HS256"
# 秘钥,默认是flask配置中的SECRET_KEY
JWT_SECRET_KEY = "F7XI/sO9jpLSQ1pDirOd3QGI/nVw+RXqydIiU6I7VdLCmvINlzWYQu8MCsNWv759"
# token令牌有效期,单位: 秒/s,默认: datetime.timedelta(minutes=15) 或者 15 * 60
JWT_ACCESS_TOKEN_EXPIRES = 60 * 10
# refresh刷新令牌有效期,单位: 秒/s,默认:datetime.timedelta(days=30) 或者 30*24*60*60
JWT_REFRESH_TOKEN_EXPIRES = 30 * 24 * 60 * 60
# 设置通过哪种方式传递jwt,默认是http请求头,也可以是query_string,json,cookies
JWT_TOKEN_LOCATION = ["headers","query_string","json"]
# 当通过http请求头传递jwt时,请求头参数名称设置,默认值: Authorization
JWT_HEADER_NAME = "Authorization"
# 当通过http请求头传递jwt时,令牌的前缀。
# 默认值为 "Bearer",例如:Authorization: Bearer <JWT>
JWT_HEADER_TYPE = "jwt"
# 当通过query string查询字符串传递jwt时,令牌参数的名称设置,默认值: jwt
JWT_QUERY_STRING_NAME = "jwt"
# 当通过json请求体传递jwt时,assess_token令牌参数的名称,默认值:access_token
JWT_JSON_KEY = "access_token"
# 当通过json请求体传递jwt时,refresh_token令牌参数的名称,默认值:access_token
JWT_REFRESH_JSON_KEY = "refresh_token"

服务端提供登陆API接口

refresh token只能用于客户端换取新的access_token,有效期会比较长

access_token只能用于服务端提供的其他api接口数据,但是有效期会很短,当access_token过期,则允许客户端凭借refresh_token来换取新的access_token。

  1. 视图: application.apps.users.api,代码:
from flask import current_app
from flask_jwt_extended import get_jwt_identity, jwt_required,create_refresh_token,create_access_token

# 引入构造器(序列化器)
from .marshmallow import MobileSchema, ValidationError, UserSchema
# 引入返回信息,和自定义状态码
from application import message, code
from . import services

# 校验手机号
def check_mobile(mobile):
    # 实例化构造器对象
    ms = MobileSchema()
    try:
        # load反序列化校验数据
        ms.load({'mobile':mobile})
        res = {'errno': code.CODE_OK, 'errmsg':message.ok}
    except ValidationError as e:
        print(e.messages) # {'mobile': ['手机号码格式有误']}
        res = {'errno': code.CODE_VALIDATE_ERROR, 'errmsg': e.messages['mobile'][0]}
    return res

# 用户注册
def register(mobile:str, password:str, password2:str, sms_code):
    """
    用户注册基本信息
    :param mobile: 手机号码
    :param password: 登录密码
    :param password2: 确认密码
    :param sms_code: 短信验证码
    :return:
    """
    # 1.验证手机是否已经注册
    res = check_mobile(mobile)
    if res['errno'] != code.CODE_OK:
        return res

    # 2.验证并保存用户信息
    try:
        urs = UserSchema()
        # 反序列化校验数据
        instance = urs.load({
            'mobile':mobile,
            'password':password,
            'password2':password2,
            'sms_code':sms_code
        })

        res = {'errno':code.CODE_OK, 'errmsg':message.ok, 'data':urs.dump(instance)}

    # 数据验证异常
    except ValidationError as e:
        # 验证码错误
        if e.messages.get('sms_code'):
            errmsg = e.messages['sms_code'][0]
        # 两次密码不一致
        elif e.messages.get('password'):
            errmsg = e.messages['password'][0]
        else:
            errmsg = message.check_data_fail

        res = {'errno':code.CODE_VALIDATE_ERROR, 'errmsg':errmsg}

    # 其他异常
    except Exception as e:
        # 打印错误日志
        current_app.log.error('服务端程序发生未知异常!')
        current_app.log.error(f'错误信息: {e}')

        res = {'errno':code.CODE_SERVER_ERROR, 'errmsg':message.server_is_error}

    return res

# 用户登录
def login(account, password):
    # 1.校验数据库是否有当前用户
    user = services.get_user_by_account(account)
    if user is None:
        return {
            'errno': code.CODE_USER_NOT_EXISTS,
            'errmsg': message.user_not_exists
        }
    # 2. 校验密码是否正确
    ret = user.check_password(password)
    if not ret:
        return {
            'errno': code.CODE_PASSWORD_ERROR,
            'errmsg': message.password_error
        }

    # 模型对象序列化成字典
    us = UserSchema()
    user_data = us.dump(user)

    # 3.生成jwt assess token 和 refresh token
    access_token = create_access_token(identity=user_data)  # identity 就是载荷
    refresh_token = create_refresh_token(identity=user_data)

    # 4.返回2个token给客户端
    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok,
        'access_token': access_token,
        'refresh_token': refresh_token
    }

"""测试"""
# 验证用户是否携带了 access_token,访问需要认证的页面时需要附带上传
@jwt_required()
def access_token():
    # 获取隐藏在jwt的载荷中的用户信息
    preload = get_jwt_identity()
    print(preload)
    return 'ok'

# 验证用户是否携带 refresh_token, 参数refresh=True则验证refresh_token
@jwt_required(refresh=True)
def refresh_token():
    # 获取隐藏在jwt的载荷中的用户信息
    preload = get_jwt_identity()
    print(preload)
    return 'ok'

  1. 数据库操作: users/services.py
from sqlalchemy import or_

from .models import db, User

# 根据手机号,姓名,邮箱获取用户信息
def get_user_by_account(account:str):
    """
    :param account: 手机号,姓名,邮箱
    :return: 模型对象
    """
    user = User.query.filter(or_(
        User.mobile == account,
        User.name == account,
        User.email == account
    )).first()
    return user
  1. 路由: users/urls.py
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api

# 蓝图路径与函数映射列表
urlpatterns = []

# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
    api_rpc('check_mobile', api.check_mobile),
    api_rpc('register', api.register),
    api_rpc('login', api.login),
    api_rpc('access', api.access_token),
    api_rpc('refresh', api.refresh_token),
]
  1. 自定义状态码 application/utils/code.py
"""自定义响应状态码"""

CODE_OK = 1000                  # 成功
CODE_VALIDATE_ERROR = 1001      # 数据验证错误
CODE_SERVER_ERROR = 1002       # 服务端程序错误
CODE_SMS_FAIL = 1003            # 短信发送失败
CODE_INTERVAL_TIME = 1004      # 短信验证码再冷却时间内
CODE_USER_NOT_EXISTS = 1005     # 用户不存在
CODE_PASSWORD_ERROR = 1006      # 密码不正确
  1. 文本提示信息 application/utils/message.py
"""提示文本信息"""

ok = '成功!!'
mobile_format_error = '手机号码格式有误'
mobile_is_use = '手机号已经被注册'
password_not_match = '两次密码输入不一致'
check_data_fail = '数据验证失败'
server_is_error = '服务端程序出错!'
sms_interval_time = '冷却时间内,验证码不能重新发送!'
sms_send_fail = '短信发送失败!'
sms_code_expired = '短信验证码已过期!'
sms_code_not_match = '验证码输入不正确!'
user_not_exists = '用户不存在!'
password_error = '密码不正确!'

测试用例

users/test.py

# 引入测试基类
from application import BasicTestCase

# 用户登录的测试用例
class CheckUserLogin(BasicTestCase):
    # 定义的函数必须是test开头
    def test_user_login(self):
        # 携带请求数据
        data = {
            # 请求方法路径
            "method": "Users.login",
            # 传入参数
            "params": {
                "account":"17600351804",
                "password":"12345678"
            }
        }

        self.post(data) # 基于父类post请求
        print(self.response) # 响应数据

        # 验证响应数据判断请求是否正确
        self.assertIn("result", self.response) # 包含
        self.assertIn("errmsg", self.response["result"])
        # 响应状态码是否相等, 1000代表成功
        self.assertEqual(1000, self.response["result"]["errno"])

自定义jwt错误信息

上面测试的时候,可以发现,当jwt有问题时,错误的提示并非我们自己返回的,而是flask_jwt_extend返回的,所以为了方便客户端识别错误,我们还需要进行这个模块里面2处的源码调整,以方便它更好的展示错误信息。

  1. flask_jwt_extended/view_decorators.py,代码:
from jwt.exceptions import DecodeError, PyJWTError
from flask_jwt_extended.exceptions import JWTExtendedException
from application.utils import code,message

def jwt_required(optional=False, fresh=False, refresh=False, locations=None):
    """
    # 省略。。。。
    """

    def wrapper(fn):
        @wraps(fn)
        def decorator(*args, **kwargs):
            # verify_jwt_in_request(optional, fresh, refresh, locations)
            try:
                verify_jwt_in_request(optional, fresh, refresh, locations)
            except DecodeError:
                # jwt不完整
                return {"errno": code.CODE_AUTOORIZATION_ERROR, "errmsg": message.authorization_is_invalid}
            except JWTExtendedException:
                # 没有jwt
                return {"errno": code.CODE_AUTOORIZATION_ERROR, "errmsg": message.no_authorization}
            except PyJWTError:
                # jwt过期
                return {"errno": code.CODE_AUTOORIZATION_ERROR, "errmsg": message.authorization_has_expired}
            return fn(*args, **kwargs)

        return decorator

    return wrapper
  1. 自定义响应状态码 application.utils.code,代码:
"""自定义响应状态码"""

CODE_OK = 1000                  # 成功
CODE_VALIDATE_ERROR = 1001      # 数据验证错误
CODE_SERVER_ERROR = 1002       # 服务端程序错误
CODE_SMS_FAIL = 1003            # 短信发送失败
CODE_INTERVAL_TIME = 1004      # 短信验证码再冷却时间内
CODE_USER_NOT_EXISTS = 1005     # 用户不存在
CODE_PASSWORD_ERROR = 1006      # 密码不正确
CODE_AUTOORIZATION_ERROR = 1007 # jwt认证失败


  1. 文本提示信息application.utils.message,代码:
"""提示文本信息"""

ok = '成功!!'
mobile_format_error = '手机号码格式有误'
mobile_is_use = '手机号已经被注册'
password_not_match = '两次密码输入不一致'
check_data_fail = '数据验证失败'
server_is_error = '服务端程序出错!'
sms_interval_time = '冷却时间内,验证码不能重新发送!'
sms_send_fail = '短信发送失败!'
sms_code_expired = '短信验证码已过期!'
sms_code_not_match = '验证码输入不正确!'
user_not_exists = '用户不存在!'
password_error = '密码不正确!'
authorization_is_invalid = "无效的认证令牌!"
no_authorization = "缺少认证令牌!"
authorization_has_expired = "认证令牌已过期!"


记录用户登录信息

  1. 视图 application.apps.users.api
from flask import current_app
from flask_jwt_extended import get_jwt_identity, jwt_required,create_refresh_token,create_access_token

# 引入构造器(序列化器)
from .marshmallow import MobileSchema, ValidationError, UserRegisterSchema
# 引入返回信息,和自定义状态码
from application import message, code
from . import services

# 校验手机号
def check_mobile(mobile):
    # 实例化构造器对象
    ms = MobileSchema()
    try:
        # load反序列化校验数据
        ms.load({'mobile':mobile})
        res = {'errno': code.CODE_OK, 'errmsg':message.ok}
    except ValidationError as e:
        print(e.messages) # {'mobile': ['手机号码格式有误']}
        res = {'errno': code.CODE_VALIDATE_ERROR, 'errmsg': e.messages['mobile'][0]}
    return res

# 用户注册
def register(mobile:str, password:str, password2:str, sms_code):
    """
    用户注册基本信息
    :param mobile: 手机号码
    :param password: 登录密码
    :param password2: 确认密码
    :param sms_code: 短信验证码
    :return:
    """
    # 1.验证手机是否已经注册
    res = check_mobile(mobile)
    if res['errno'] != code.CODE_OK:
        return res

    # 2.验证并保存用户信息
    try:
        urs = UserSchema()
        # 反序列化校验数据
        instance = urs.load({
            'mobile':mobile,
            'password':password,
            'password2':password2,
            'sms_code':sms_code
        })

        res = {'errno':code.CODE_OK, 'errmsg':message.ok, 'data':urs.dump(instance)}

    # 数据验证异常
    except ValidationError as e:
        # 验证码错误
        if e.messages.get('sms_code'):
            errmsg = e.messages['sms_code'][0]
        # 两次密码不一致
        elif e.messages.get('password'):
            errmsg = e.messages['password'][0]
        else:
            errmsg = message.check_data_fail

        res = {'errno':code.CODE_VALIDATE_ERROR, 'errmsg':errmsg}

    # 其他异常
    except Exception as e:
        # 打印错误日志
        current_app.log.error('服务端程序发生未知异常!')
        current_app.log.error(f'错误信息: {e}')

        res = {'errno':code.CODE_SERVER_ERROR, 'errmsg':message.server_is_error}

    return res

# 用户登录
def login(account, password):
    # 1.校验数据库是否有当前用户
    user = services.get_user_by_account(account)
    if user is None:
        return {
            'errno': code.CODE_USER_NOT_EXISTS,
            'errmsg': message.user_not_exists
        }
    # 2. 校验密码是否正确
    ret = user.check_password(password)
    if not ret:
        return {
            'errno': code.CODE_PASSWORD_ERROR,
            'errmsg': message.password_error
        }

    # 记录用户本次登录信息
    services.save_user_login_info(user)

    # 模型对象序列化成字典
    us = UserSchema()
    user_data = us.dump(user)

    # 3.生成jwt assess token 和 refresh token
    access_token = create_access_token(identity=user_data)  # identity 就是载荷
    refresh_token = create_refresh_token(identity=user_data)

    # 4.返回2个token给客户端
    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok,
        'access_token': access_token,
        'refresh_token': refresh_token
    }

"""测试"""
# 验证用户是否携带了 access_token,访问需要认证的页面时需要附带上传
@jwt_required()
def access_token():
    # 获取隐藏在jwt的载荷中的用户信息
    preload = get_jwt_identity()
    print(preload)
    return 'ok'

# 验证用户是否携带 refresh_token
@jwt_required(refresh=True)
def refresh_token():
    # 获取隐藏在jwt的载荷中的用户信息
    preload = get_jwt_identity()
    print(preload)
    return 'ok'


  1. 数据服务层,application.apps.users.services,代码:
from sqlalchemy import or_
from datetime import datetime
from flask import request

from .models import db, User
from application.utils.iptools import get_address_by_ip

# 根据手机号码获取用户信息
def get_user_by_mobile(mobile:str)->User:
    """
    :param mobile: 手机号码
    :return: 用户模型对象
    """
    user = User.query.filter(User.mobile == mobile).first()
    return user

# 添加用户信息
def add_user(data:dict)->User:
    """
    新增用户信息
    :param data: 用户信息 - 字典类型
    :return: 用户模型对象
    """
    instance = User(**data)
    db.session.add(instance)
    db.session.commit()
    return instance

# 根据手机号,姓名,邮箱获取用户信息
def get_user_by_account(account:str):
    """
    :param account: 手机号,姓名,邮箱
    :return: 模型对象
    """
    user = User.query.filter(or_(
        User.mobile == account,
        User.name == account,
        User.email == account
    )).first()
    return user

# 记录用户登录信息
def save_user_login_info(user):
    """
    记录本次用户登录的相关信息
    :param user:
    :return:
    """
    # 本次登录时间
    user.updated_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    # 本次登录IP地址
    user.ip_address = request.remote_addr

    # 本次登录的地理位置(通过ip地址)
    ip_data = get_address_by_ip(user.ip_address)
    user.province = ip_data.get("province")
    user.city = ip_data.get("city")
    user.area = ip_data.get("district")

    db.session.commit() # 提交保存到数据库
  1. 获取地理位置信息application/utils/iptools.py,代码:
import orjson, requests, random
from flask import current_app

# 根据IP地址获取地理位置信息
def get_address_by_ip(ip:str)->dict:
    """
    根据IP地址获取地理位置信息
    :param ip: ip地址
    :return: 字典
    """
    # 判断当前是开发还是生产模式,如果是开发模式
    if current_app.config.get('DEBUG'):
        ip_list = current_app.config.get("AMAP_TEST_IP")
        ip = ip_list[random.randint(0, len(ip_list) - 1)]

    # 请求路径
    url = f"{current_app.config.get('AMAP_GATEWAY')}?key={current_app.config.get('AMAP_KEY')}&type={current_app.config.get('AMAP_IP_TYPE')}&ip={ip}"
    # 发送get请求,得到响应数据对象
    response = requests.get(url)
    print(f"response={response}") # response=<Response [200]>

    # 返回结果是json格式字符串,转换成字典
    ip_data = orjson.loads(response.content)
    if ip_data.get("status") == "1":  # 判断返回状态,1是成功
        return {
            "province": ip_data.get("province"),
            "city": ip_data.get("city"),
            "district": ip_data.get("district"),
            "location": ip_data.get("location"),  # 经纬度
        }
    else:
        return {}
  1. 开发环境配置application/settings/dev.py,代码:
"""高德地图api接口"""
# IP查询地址的网关地址
AMAP_GATEWAY = "https://restapi.amap.com/v5/ip"
# 应用秘钥
AMAP_KEY = "6c9474e7c520ec8c2c1030dd83140e74"
# IP版本:4表示IPV4
AMAP_IP_TYPE = 4
# 本次测试时使用的IP地址
AMAP_TEST_IP = [
    "221.218.212.11",
    "221.213.112.11",
    "221.215.215.12",
    "221.217.212.12",
    "221.214.215.12",
]

客户端提交登录信息

  1. 发送登陆请求 html/login.html,代码:
<!DOCTYPE html>
<html>
<head>
	<title>登录</title>
	<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
	<meta charset="utf-8">
	<link rel="stylesheet" href="../static/css/main.css">
	<script src="../static/js/vue.js"></script>
  <script src="../static/js/axios.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/main.js"></script>
</head>
<body>
	<div class="app" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="bg">
			<img src="../static/images/bg0.jpg">
		</div>
		<div class="form">
			<div class="form-title">
				<img src="../static/images/login.png">
				<img class="back" @click='backpage' src="../static/images/back.png">
			</div>
			<div class="form-data">
				<div class="form-data-bg">
					<img src="../static/images/bg1.png">
				</div>
				<div class="form-item">
					<label class="text">手机</label>
					<input type="text" v-model="account" placeholder="请输入手机号/姓名/邮箱">
				</div>
				<div class="form-item">
					<label class="text">密码</label>
					<input type="password" v-model="password" placeholder="请输入密码">
				</div>
				<div class="form-item">
					<input type="checkbox" class="agree remember" v-model="agree" >
					<label><span class="agree_text ">记住密码,下次免登录</span></label>
				</div>
				<div class="form-item">
					<img class="commit" @click="loginhandle" src="../static/images/commit.png">
				</div>
				<div class="form-item">
					<p class="toreg" @click='to_register'>立即注册</p>
					<p class="tofind">忘记密码</p>
				</div>
			</div>
		</div>
	</div>
	<script>
	apiready = function(){
    var game = new Game("../static/mp3/bg2.mp3");
    Vue.prototype.game = game;
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,
					account:"17600351804",
					password:"12345678",
					agree:false, // 是否记住密码
				}
			},
			watch:{
        music_play(){
          if(this.music_play){
            this.game.play_music("../static/mp3/bg2.mp3");
          }else{
            this.game.stop_music();
          }
        },

      },
			methods:{
        // 跳转注册页面
        to_register(){
          this.game.openFrame('register','register.html')
        },
				// 返回上一页,本质是关闭当前页面
				backpage(){
					this.game.closeWin()
				},
				// 登陆处理
				loginhandle(){
					// 点击按钮声音
					this.game.play_music('../static/mp3/btn1.mp3')
					// 判断账号密码不能为空
					if(this.account.length < 1 || this.password.length < 1){
						this.game.tips('账号或密码不能为空!');
						return false;
					}
					// 发送登陆请求
					let self = this;
					self.game.post(self,{
						"method":"Users.login",
						"params":{
							"account": self.account,
							"password": self.password
						},
						success(response){
							let data = response.data;
							if((data.result && data.result.errno === 1005) || (data.result && data.result.errno === 1006)){
								self.game.tips('账号或密码不正确!')
							}
							if(data.result && data.result.errno === 1000){
								// 登陆成功
								self.game.tips('登陆成功!')
								// 保存认证令牌
								if(self.agree){ // 记住登陆状态
									// 令牌保存到本地文件
									self.game.setfs({
										"access_token": data.result.access_token,
										"refresh_token": data.result.refresh_token
									});
									// 清除内存中缓存零牌数据
									self.game.deldata(["access_token", "refresh_token"]);
								}else {
									// 令牌保存到内存中
									self.game.setdata({
										"access_token": data.result.access_token,
										"refresh_token": data.result.refresh_token
									});
									// 清除系统文件缓存令牌数据
									self.game.delfs(["access_token", "refresh_token"])
								}
								// 跳转到用户中心页面
								setTimeout(() => {
									self.game.openWin("user", "user.html")
									// 发送事件广播登陆成功,第二个参数就是服务端返回的data数据
									setTimeout(() => {
										// 测试是否存储token值
										// self.game.print(self.game.getfs(["access_token", "refresh_token"]))
										// self.game.print(self.game.getdata(["access_token", "refresh_token"]))
										self.game.get_user_by_token(data.result.access_token)
										self.game.sendEvent("user_login_success", data.result)
									}, 500);
								}, 2000);
							}
						}
					});
				},

			}
		})
	}
	</script>
</body>
</html>

使用sessionStorage会存在不同窗口没办法共享数据的情况,所以我们需要使用由APICloud提供的数据存储api接口来保存登陆信息。

保存用户登录状态 - 从token(载荷)base64提取用户信息

基于APICloud提供的本地存储可以有效保存数据

// 保存数据到内存中
api.setGlobalData({
    key: 'userName',
    value: 'api'
});
// 从内存中获取数据
var userName = api.getGlobalData({
    key: 'userName'
});

// 保存数据到系统文件中
api.setPrefs({ // 储存
    key: 'userName',
    value: 'api'
});
// 从系统文件中获取数据
api.getPrefs({//获取
    key: 'userName'
}, function(ret, err) {
    ...
});
    
// 注意:基于api.getPrefs获取数组时,会出现转义格式的字符
api.removePrefs({// 从系统文件中删除数据
    key: 'userName'
});
  1. 封装对于api对象提供的存储数据方法,static/js/main.js,代码:
class Game{
	constructor(bg_music){
		// 构造函数,相当于python中类的__init__方法
		this.init();
		if(bg_music){
    	this.play_music(bg_music);
		}
	}
	init(){
		// 初始化
		console.log("系统初始化");
    this.rem(); // 自适配方案,根据当前受屏幕,自动调整rem单位
		this.init_config(); //初始化配置
		this.init_http(); // 初始化http网络请求
	}

  rem(){
    if(window.innerWidth<1200){
			this.UIWidth = document.documentElement.clientWidth;
      this.UIHeight = document.documentElement.clientHeight;
      document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
			document.querySelector("#app").style.height=this.UIHeight+"px"
    }
    window.onresize = ()=>{
      if(window.innerWidth<1200){
        this.UIWidth = document.documentElement.clientWidth;
        this.UIHeight = document.documentElement.clientHeight;
        document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
      }
    }
  }

	// 初始化配置
	init_config(){
		// 客户端项目的全局配置
		this.config = {
			// 服务端API地址
			API_SERVER:"http://192.168.189.138:5000/api",
			SMS_TIME_OUT: 60 , // 短信发送冷却时间/秒
		}
	}

	// 初始化http网络请求
	init_http(){
		// ajax初始化
		if(window.axios){
			axios.defaults.baseURL = this.config.API_SERVER // 接口网关地址
			axios.defaults.timeout = 2500 // 请求超时时间
			axios.defaults.withCredentials = false // 跨域请求时禁止携带cookie
			// 请求拦截器和响应拦截器相当于中间件作用
			// 1. 添加请求拦截器
			axios.interceptors.request.use((config) => {
				// 请求正确
				// 在发送请求之前的初始化[添加请求头],config就是本次请求数据对象
				// 显示进度提示框
				api.showProgress({
				    style: 'default', 	// 进度提示框风格
				    animationType: 'zoom', // 动画类型 缩放
				    title: '努力加载中...', // 标题
				    text: '请稍等...',		// 内容
				    modal: true  //是否模态,模态时整个页面将不可交互
				});
				return config // 返回对象

			}, (error) => {
				// 请求错误, 隐藏进度提示框
				api.hideProgress();
				// 弹出错误提示框
				this.tips("网络错误!!");

				// 返回
				return Promise.reject(error);
			});

			// 2. 添加响应拦截器 - 找出响应数据错误
			axios.interceptors.response.use((response) => {
				// 关闭进度提示框
				api.hideProgress();
				// 判断接口返回状态码
				if(response.data && response.data.error){
					// 服务器报错
					let error = response.data.error;
					switch (error.code) {
						case -32601: // 请求接口不存在
							this.tips("请求地址不存在!");

							break;
						case 500:
							this.tips("服务端程序执行错误!\n" + error.message);

							break;
					}

					if(response.data && response.data.result){
						// 判断请求唯一标识是否一致
						if(axios.uuid != response.data.id){
							this.tips("请求拦截错误!");

							return false;
						}
					}

					let result = response.data.resut;
					if(result.errno != 1000){
						api.toast(this.tips(result.errmsg));
					}
				//	return response  // 没有错误的话,返回响应数据
				}
				return response  // 没有错误的话,返回响应数据

			}, (error) => {
				// 关闭进度提示框
				api.hideProgress();
				// 网络错误提示
				switch (error.message) {
					case "Network Error":
						this.tips('网络错误!!');

						break;
				}
				return Promise.reject(error);

			});

			if(Vue){
				// js语法: prototype 向对象添加属性和方法
				Vue.prototype.axios = axios;
			}

			if(window.UUID){
				// prototype 向对象添加属性和方法
				Vue.prototype.uuid = UUID.generate;
			}

		}

	}

	// 窗口提示
	tips(msg, duration = 5000, location = "top"){
		// 参数: 提示信息 - 时间  - 显示位置(上中下)
		let params = {
			msg: msg,
			duration: duration,
			location: location
		}
		api.toast(params)
	}

	// 网络发送post请求
	post(vm, data){
		// 基于axios发送post请求
		vm.axios.uuid = vm.uuid();
		vm.axios.post("", {
			"jsonrpc": "2.0",
			"id": vm.axios.uuid,
			"method": data.method,
			"params": data.params
		}, data.header).then(
			data.success
		).catch(
			data.fail
		);
	}

	print(data){
		// 打印数据
		console.log(JSON.stringify(data));
	}
	stop_music(){
		this.print("停止背景音乐");
		document.body.removeChild(this.audio);
	}
  play_music(src){
		this.print("播放背景音乐");
    this.audio = document.createElement("audio");
    this.source = document.createElement("source");
    this.source.type = "audio/mp3";
    this.audio.autoplay = "autoplay";
    this.source.src=src;
    this.audio.appendChild(this.source);

		document.body.appendChild(this.audio);

		// 自动暂停关闭背景音乐
    var t = setInterval(()=>{
      if(this.audio.readyState > 0){
        if(this.audio.ended){
          clearInterval(t);
          document.body.removeChild(this.audio);
        }
      }
    },100);
  }

	//创建窗口
	openWin(name,url,pageParam){
		if(!pageParam){
			pageParam = {}
		}
		api.openWin({
			name: name,           // 自定义窗口名称,如果是新建窗口,则名字必须是第一次出现的名字。
			bounces: false,       // 窗口是否上下拉动
			reload: true,         // 如果窗口已经在之前被打开了,是否要重新加载当前窗口中的页面
			url: url,             // 窗口创建时展示的html页面的本地路径[相对于当前代码所在文件的路径]
			animation:{           // 打开新建窗口时的过渡动画效果
				type:"push",                //动画类型(详见动画类型常量)
				subType:"from_right",       //动画子类型(详见动画子类型常量)
				duration:300               //动画过渡时间,默认300毫秒
			},
			pageParam: pageParam,
		});
	}

	// 关闭指定窗口
	closeWin(name=''){
		let params
		if(name !== ''){
			params = {
				name:name,
			}
		}
		api.closeWin(params);
	}

	// 创建帧页面
	openFrame(name,url,pageParam){
		if(!pageParam){
			pageParam = {}
		}
		api.openFrame({
			name: name,		// 帧页面的名称
			url: url,	// 帧页面打开的url地址
			bounces:false,        // 页面是否可以下拉拖动
			reload: true,         // 帧页面如果已经存在,是否重新刷新加载
			useWKWebView:true,    // 是否使用WKWebView来加载页面
			historyGestureEnabled:true,  // 是否可以通过手势来进行历史记录前进后退,只在useWKWebView参数为true时有效
			vScrollBarEnabled: false,	// 是否显示垂直滚动条
			hScrollBarEnabled: false,	// 是否显示水平滚动条

			animation:{
					type:"push",             //动画类型(详见动画类型常量)
				subType:"from_right",    //动画子类型(详见动画子类型常量)
				duration:300             //动画过渡时间,默认300毫秒
			},
			rect: {               // 当前帧的宽高范围
					// 方式1,设置矩形大小宽高
					x: 0,             // 左上角x轴坐标
					y: 0,             // 左上角y轴坐标
					w: 'auto',        // 当前帧页面的宽度, auto表示满屏
					h: 'auto'         // 当前帧页面的高度, auto表示满屏
			// 方式2,设置矩形大小宽高
					// marginLeft:,    //相对父页面左外边距的距离,数字类型
					// marginTop:,     //相对父页面上外边距的距离,数字类型
					// marginBottom:,  //相对父页面下外边距的距离,数字类型
					// marginRight:    //相对父页面右外边距的距离,数字类型
			},
			pageParam: {          // 要传递新建帧页面的参数,在新页面可通过 api.pageParam.name 获取
					name: pageParam      // name只是举例, 可以传递任意自定义参数
			}
	});
	}

	// 关闭帧页面
	closeFrame(name=''){
		let params
		if(name !== ''){
			params = {
				name: name
			}
		}
		api.closeFrame(params);
	}

	// 发送APP全局事件通知/全局事件广播
	sendEvent(name, data){
		api.sendEvent({
		    name: name,
		    extra: data
		});
	}

	// 保存数据到本地文件系统
	setfs(data){ // data 是一个字典
		for(let key in data){
			api.setPrefs({
			    key: key,
			    value: data[key]
			});
		}
	}
	// 根据key值来获取本地文件系统中存储的数据
	getfs(key){ // key="access_token"    key = ["access_token","refresh_token"]
		// key值统统转化成数组类型
		let keys = key;
		if(!(key instanceof Array)){
			keys = [key]
		}
		let data = {};
		for(let item of keys){
			data[item] = api.getPrefs({
			    key: item,
					sync: true  //执行结果的返回方式。为false时通过callback返回,为true时直接返回
			});
		}
		if(key instanceof Array){
			// 返回数组
			return data
		}
		// 返回单个数据
		return data[key]
	}
	// 根据key值来删除本地文件系统中存储的数据
	delfs(key){
		// key值统统转化成数组类型
		let keys = key;
		if(!(key instanceof Array)){
			keys = [key]
		}
		for(let item of keys){
			api.removePrefs({
			    key: item
			});
		}
	}

	// 保存数据到内存中
	setdata(data){
		for(let key in data){
			api.setGlobalData({
				key:key,
				value:data[key]
			});
		}
	}
	// 根据key值来获取内存中存储的数据
	getdata(key){
		// 转化成数组数据处理
		let keys = key;
		if(!(key instanceof Array)){
			keys = [key];
		}
		let data = {};
		for(let item of keys){
			data[item] = api.getGlobalData({
				key:item
			});
		}
		// 返回结果
		if(key instanceof Array){
			return data; // 返回数组
		}
		return data[key] // 返回单个数据
	}
	// 根据key值来删除内存中保存的数据
	// 因为本身并没有提供删除内存数据的方法,所以此处我们把设置为null即可
	deldata(key){
		// 转化成数组数据处理
		let keys = key;
		if(!(key instanceof Array)){
			keys = [key];
		}
		// 设置数值为null
		for(let item of keys){
			api.setGlobalData({
				key:null
			});
		}
	}

	// 根据token令牌载荷获取登陆用户信息
	get_user_by_token(token){
		let preload = token.split('.')[1];
		// 在服务端中发送给客户端的token是经过base64编码处理的,获取需解码
		// btoa 字符串---->编码--->base64编码
	  // atob base64--->解码--->字符串
		let data_str = atob(preload);
		let data = JSON.parse(data_str)
		this.print(data)
		// 打印出的数据, sub中存有用户信息
		// {"fresh":false,"iat":1623162593,"jti":"3993f774-870f-45b8-b547-71c40f7fefd1",
		// "type":"access","sub":{"id":16,"name":"曹军"},"nbf":1623162593,"exp":1623163193}
		return data.sub
	}

}

在APPCloud中集成防水墙验证码

官网: https://007.qq.com

验证码控制台登陆地址: https://console.cloud.tencent.com/captcha

快速接入:https://007.qq.com/python-access.html?ADTAG=acces.start

把验证码应用的ID和秘钥添加到application/settings/dev.py配置文件中.

# 防水墙验证码
CAPTCHA_GATEWAY="https://ssl.captcha.qq.com/ticket/verify"
CAPTCHA_APP_ID="2028945858"
CAPTCHA_APP_SECRET_KEY="0NQ7-794BcsTbIIivHHmwiw**"

前端获取显示并校验验证码

下载地址:https://ssl.captcha.qq.com/TCaptcha.js

防水墙的前端核心js文件TCaptcha.js引入到客户端项目的静态文件下static/js/TCaptcha.js

在客户端项目的static/js/main.js中init_config方法中添加配置CAPTCHA_APP_ID配置。

class Game {
    // ....
    init_config() {
        // 客户端项目的全局配置
        this.config = {
            API_SERVER: "http://192.168.233.129:5000/api", // 服务端API地址
            SMS_TIME_OUT: 60,  // 短信发送冷却时间/秒
            CAPTCHA_APP_ID: "2028945858",  // 防水墙验证码的应用ID
        }
    }
    // ....
}

客户端展示验证码

  1. 登陆页面添加防水墙验证码 : html/login.html,代码:
<!DOCTYPE html>
<html>
<head>
	<title>登录</title>
	<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
	<meta charset="utf-8">
	<link rel="stylesheet" href="../static/css/main.css">
	<script src="../static/js/vue.js"></script>
  <script src="../static/js/axios.js"></script>
  <script src="../static/js/uuid.js"></script>
	<script src="../static/js/main.js"></script>
	<script src="../static/js/TCaptcha.js"></script>

</head>
<body>
	<div class="app" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="bg">
			<img src="../static/images/bg0.jpg">
		</div>
		<div class="form">
			<div class="form-title">
				<img src="../static/images/login.png">
				<img class="back" @click="backpage" src="../static/images/back.png">
			</div>
			<div class="form-data">
				<div class="form-data-bg">
					<img src="../static/images/bg1.png">
				</div>
				<div class="form-item">
					<label class="text">账号</label>
					<input type="text" v-model="account" placeholder="请输入手机号/用户名/邮箱">
				</div>
				<div class="form-item">
					<label class="text">密码</label>
					<input type="password" v-model="password" placeholder="请输入密码">
				</div>
				<div class="form-item">
					<input type="checkbox" class="agree remember" v-model="agree">
					<label><span class="agree_text ">记住密码,下次免登录</span></label>
				</div>
				<div class="form-item">
					<img class="commit" @click="show_captcha" src="../static/images/commit.png">
				</div>
				<div class="form-item">
					<p class="toreg" @click="open_register">立即注册</p>
					<p class="tofind">忘记密码</p>
				</div>
			</div>
		</div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					account:"13928835901",
					password:"123456",
					agree: true,  // 是否记住密码
          music_play:true,
				}
			},
			watch:{
        music_play(){
          if(this.music_play){
            this.game.play_music("../static/mp3/bg1.mp3");
          }else{
            this.game.stop_music();
          }
        }
      },
			methods:{
				open_register(){
					// 打开注册页面
					this.game.openFrame("register","register.html");
				},
				backpage(){
					this.game.closeWin();
				},
				show_captcha(){
					// 验证码校验
					// 显示验证码
					var captcha1 = new TencentCaptcha(this.game.config.CAPTCHA_APP_ID, (res)=>{
						if(res.ret==0){
							// 验证码验证成功,返回4个数据,ret,appid,ticket和randstr
							this.loginhandler(res.ticket,res.randstr); // 提交登录数据
						}
					});
					captcha1.show(); // 显示验证码
				},
				loginhandler(){
					// 登陆处理
					// 按钮声音
					this.game.play_music('../static/mp3/btn1.mp3');
					// 验证账号和密码不能为空!
					if( this.account.length<1 || this.password.length < 1 ){
						this.game.tips("账号或密码不能为空!");
						return false;
					}

					// 发送登陆请求
         			 var self = this;
					this.game.post(this,{
						"method": "Users.login",
						"params": {
							"account": this.account,
							"password": this.password
						},
						success(response){
							var data = response.data;
							if((data.result && data.result.errno === 1005) || (data.result && data.result.errno === 1006)){
								self.game.tips('账号或密码不正确!')
							}
							if(data.result && data.result.errno == 1000){
								// 登陆成功
								self.game.tips("登陆成功!");
								// 保存认证令牌
								if(self.agree){ // 记住登陆状态
									// 令牌保存到本地文件
									self.game.setfs({
										"access_token": data.result.access_token,
										"refresh_token": data.result.refresh_token
									});
									// 清除内存中缓存零牌数据
									self.game.deldata(["access_token", "refresh_token"]);
								}else {
									// 令牌保存到内存中
									self.game.setdata({
										"access_token": data.result.access_token,
										"refresh_token": data.result.refresh_token
									});
									// 清除系统文件缓存令牌数据
									self.game.delfs(["access_token", "refresh_token"])
								}

								// 测试:本地获取登陆用户信息
								self.game.get_user_by_token(data.result.access_token);
								setTimeout(() => {
									// 打开用户中心窗口
									self.game.openWin("user", "user.html");
									setTimeout(() => {
										// 发送事件广播,第二个参数就是服务端返回的data数据
										self.game.sendEvent("user_login_success", response.data.result);
									}, 500);
								}, 3000);
							}
						}
					});

				}
			}
		})
	}
	</script>
</body>
</html>
  1. 在前端用户完成验证码的校验工作以后,调整登陆方法,然后附带校验结果到服务端。

html/login.html,代码:

<!DOCTYPE html>
<html>
<head>
	<title>登录</title>
	<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
	<meta charset="utf-8">
	<link rel="stylesheet" href="../static/css/main.css">
	<script src="../static/js/vue.js"></script>
  <script src="../static/js/axios.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/main.js"></script>
	<script src="../static/js/TCaptcha.js"></script>
</head>
<body>
	<div class="app" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="bg">
			<img src="../static/images/bg0.jpg">
		</div>
		<div class="form">
			<div class="form-title">
				<img src="../static/images/login.png">
				<img class="back" @click='backpage' src="../static/images/back.png">
			</div>
			<div class="form-data">
				<div class="form-data-bg">
					<img src="../static/images/bg1.png">
				</div>
				<div class="form-item">
					<label class="text">账号</label>
					<input type="text" v-model="account" placeholder="请输入手机号/姓名/邮箱">
				</div>
				<div class="form-item">
					<label class="text">密码</label>
					<input type="password" v-model="password" placeholder="请输入密码">
				</div>
				<div class="form-item">
					<input type="checkbox" class="agree remember" v-model="agree" >
					<label><span class="agree_text ">记住密码,下次免登录</span></label>
				</div>
				<div class="form-item">
					<img class="commit" @click="show_captcha" src="../static/images/commit.png">
				</div>
				<div class="form-item">
					<p class="toreg" @click='to_register'>立即注册</p>
					<p class="tofind">忘记密码</p>
				</div>
			</div>
		</div>
	</div>
	<script>
	apiready = function(){
    var game = new Game("../static/mp3/bg2.mp3");
    Vue.prototype.game = game;
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,
					account:"17600351804",
					password:"12345678",
					agree:false, // 是否记住密码
				}
			},
			watch:{
        music_play(){
          if(this.music_play){
            this.game.play_music("../static/mp3/bg2.mp3");
          }else{
            this.game.stop_music();
          }
        },

      },
			methods:{
        // 跳转注册页面
        to_register(){
          this.game.openFrame('register','register.html')
        },
				// 返回上一页,本质是关闭当前页面
				backpage(){
					this.game.closeWin()
				},
				// 显示防水墙验证码
				show_captcha(){
					// 初始化验证码实例
					let captcha = new TencentCaptcha(this.game.config.CAPTCHA_APP_ID, (res) => {
						if(res.ret == 0){
							// 验证码验证成功,返回4个数据:
							// 1.ret-验证结果 0 成功,
							// 2.CaptchaAppId 验证码应用id,
							// 3.ticket 验证成功的票据
							// 4.randstr 本次验证的随机串,请求后台接口时需带上
							// 提交登录数据 - 向后台验证
							this.loginhandle(res.ticket,res.randstr);
						}
					});
					// 显示验证码
					captcha.show();
				},

				// 登陆处理
				loginhandle(ticket, randstr){
					// 点击按钮声音
					this.game.play_music('../static/mp3/btn1.mp3')
					// 判断账号密码不能为空
					if(this.account.length < 1 || this.password.length < 1){
						this.game.tips('账号或密码不能为空!');
						return false;
					}
					// 发送登陆请求
					let self = this;
					self.game.post(self,{
						"method":"Users.login",
						"params":{
							"account": self.account,
							"password": self.password,
							"ticket": ticket,
							"randstr": randstr
						},
						success(response){
							let data = response.data;
							if((data.result && data.result.errno === 1005) || (data.result && data.result.errno === 1006)){
								self.game.tips('账号或密码不正确!')
							}
							if(data.result && data.result.errno === 1000){
								// 登陆成功
								self.game.tips('登陆成功!')
								// 保存认证令牌
								if(self.agree){ // 记住登陆状态
									// 令牌保存到本地文件
									self.game.setfs({
										"access_token": data.result.access_token,
										"refresh_token": data.result.refresh_token
									});
									// 清除内存中缓存零牌数据
									self.game.deldata(["access_token", "refresh_token"]);
								}else {
									// 令牌保存到内存中
									self.game.setdata({
										"access_token": data.result.access_token,
										"refresh_token": data.result.refresh_token
									});
									// 清除系统文件缓存令牌数据
									self.game.delfs(["access_token", "refresh_token"])
								}
								// 跳转到用户中心页面
								setTimeout(() => {
									self.game.openWin("user", "user.html")
									// 发送事件广播登陆成功,第二个参数就是服务端返回的data数据
									setTimeout(() => {
										// 测试是否存储token值
										// self.game.print(self.game.getfs(["access_token", "refresh_token"]))
										// self.game.print(self.game.getdata(["access_token", "refresh_token"]))
										//self.game.get_user_by_token(data.result.access_token)
										self.game.sendEvent("user_login_success", data.result)
									}, 500);
								}, 2000);
							}
						}
					});
				},

			}
		})
	}
	</script>
</body>
</html>

服务端登陆接口中校验验证码回调是否正确

  1. 封装了一个验证码验证工具函数,application/utils/captcha.py,代码:
import orjson

from urllib.request import urlopen
from urllib.parse import urlencode
from flask import current_app

from application import message

# 自定义异常类
class CaptchaError(Exception):
    pass
class CaptchaParamsError(CaptchaError):
    """参数异常"""
    pass
class CaptchaNetWorkError(CaptchaError):
    """网络异常"""
    pass

class CaptchaFailError(CaptchaError):
    """验证失败"""
    pass

# 防水墙验证码的验证回调函数方法
def check_captcha(ticket, randstr, user_ip):
    if len(ticket) < 1 or len(randstr) < 1:
        raise CaptchaParamsError

    try:
        # 验证必须写到的5个参数,名字固定,不许更改
        params = {
            "aid": current_app.config.get('CAPTCHA_APP_ID'),
            "AppSecretKey": current_app.config.get('CAPTCHA_APP_SECRET_KEY'),
            "Ticket": ticket,
            "Randstr": randstr,
            "UserIP": user_ip
        }
    except Exception:
        # 参数异常
        raise CaptchaParamsError

    try:
        # 把字典类型参数转换成url路径参数 a=1&b=2&c=3
        params = urlencode(params).encode(encoding='utf-8')
        # 发送get请求,获取响应数据 - json格式
        response = urlopen(current_app.config.get('CAPTCHA_GATEWAY'), params).read()
        # 把响应数据转化成字典 - response为1,验证成功
        data = orjson.loads(response)
    except:
        # 网络异常
        raise CaptchaNetWorkError

    # 判断是否验证成功
    if data['response'] != '1':
        raise CaptchaFailError
  1. 视图调用验证码验证工具函数 users/api.py,,代码:
from flask import current_app, request
from flask_jwt_extended import get_jwt_identity, jwt_required,create_refresh_token,create_access_token

# 引入构造器(序列化器)
from .marshmallow import MobileSchema, ValidationError, UserSchema
# 引入返回信息,和自定义状态码
from application import message, code
from . import services
from application.utils import captcha

# 校验手机号
def check_mobile(mobile):
    # 实例化构造器对象
    ms = MobileSchema()
    try:
        # load反序列化校验数据
        ms.load({'mobile':mobile})
        res = {'errno': code.CODE_OK, 'errmsg':message.ok}
    except ValidationError as e:
        print(e.messages) # {'mobile': ['手机号码格式有误']}
        res = {'errno': code.CODE_VALIDATE_ERROR, 'errmsg': e.messages['mobile'][0]}
    return res

# 用户注册
def register(mobile:str, password:str, password2:str, sms_code):
    """
    用户注册基本信息
    :param mobile: 手机号码
    :param password: 登录密码
    :param password2: 确认密码
    :param sms_code: 短信验证码
    :return:
    """
    # 1.验证手机是否已经注册
    res = check_mobile(mobile)
    if res['errno'] != code.CODE_OK:
        return res

    # 2.验证并保存用户信息
    try:
        urs = UserSchema()
        # 反序列化校验数据
        instance = urs.load({
            'mobile':mobile,
            'password':password,
            'password2':password2,
            'sms_code':sms_code
        })

        res = {'errno':code.CODE_OK, 'errmsg':message.ok, 'data':urs.dump(instance)}

    # 数据验证异常
    except ValidationError as e:
        # 验证码错误
        if e.messages.get('sms_code'):
            errmsg = e.messages['sms_code'][0]
        # 两次密码不一致
        elif e.messages.get('password'):
            errmsg = e.messages['password'][0]
        else:
            errmsg = message.check_data_fail

        res = {'errno':code.CODE_VALIDATE_ERROR, 'errmsg':errmsg}

    # 其他异常
    except Exception as e:
        # 打印错误日志
        current_app.log.error('服务端程序发生未知异常!')
        current_app.log.error(f'错误信息: {e}')

        res = {'errno':code.CODE_SERVER_ERROR, 'errmsg':message.server_is_error}

    return res

# 用户登录
def login(account, password, ticket, randstr):
    """
    用户登录
    :param account: 用户账号
    :param password: 用户密码
    :param ticket: 验证码客户端验证回调的票据
    :param randstr: 验证码客户端验证回调的随机串
    :return: 
    """
        
    # 0.验证防水墙验证码是否成功
    try:
        captcha.check_captcha(ticket, randstr, request.remote_addr)
    except captcha.CaptchaParamsError: # 参数异常
        return {
            "errno": code.CODE_PARAMS_ERROR,
            "errmsg": message.params_error
        }
    except captcha.CaptchaNetWorkError: # 网络异常
        return {
            "errno": code.CODE_NETWORK_ERROR,
            "errmsg": message.network_error
        }
    except captcha.CaptchaFailError: # 数据校验失败
        return {
            "errno": code.CODE_VALIDATE_ERROR,
            "errmsg": message.check_data_fail
        }
    except Exception as e:
        current_app.log.error(f'服务端程序出错: {e}')
        return {
            "errno": code.CODE_SERVER_ERROR,
            "errmsg": message.server_is_error
        }


    # 1.校验数据库是否有当前用户
    user = services.get_user_by_account(account)
    if user is None:
        return {
            'errno': code.CODE_USER_NOT_EXISTS,
            'errmsg': message.user_not_exists
        }
    # 2. 校验密码是否正确
    ret = user.check_password(password)
    if not ret:
        return {
            'errno': code.CODE_PASSWORD_ERROR,
            'errmsg': message.password_error
        }

    # 记录用户本次登录信息
    services.save_user_login_info(user)

    # 模型对象序列化成字典
    us = UserSchema()
    user_data = us.dump(user)

    # 3.生成jwt assess token 和 refresh token
    access_token = create_access_token(identity=user_data)  # identity 就是载荷
    refresh_token = create_refresh_token(identity=user_data)

    # 4.返回2个token给客户端
    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok,
        'access_token': access_token,
        'refresh_token': refresh_token
    }

"""测试"""
# 验证用户是否携带了 access_token,访问需要认证的页面时需要附带上传
@jwt_required()
def access_token():
    # 获取隐藏在jwt的载荷中的用户信息
    preload = get_jwt_identity()
    print(preload)
    return 'ok'

# 验证用户是否携带 refresh_token
@jwt_required(refresh=True)
def refresh_token():
    # 获取隐藏在jwt的载荷中的用户信息
    preload = get_jwt_identity()
    print(preload)
    return 'ok'

  1. 错误提示码和提示语信息

错误提示码 application/utils/code.py

"""自定义响应状态码"""

CODE_OK = 1000                  # 成功
CODE_VALIDATE_ERROR = 1001      # 数据验证错误
CODE_SERVER_ERROR = 1002       # 服务端程序错误
CODE_SMS_FAIL = 1003            # 短信发送失败
CODE_INTERVAL_TIME = 1004      # 短信验证码再冷却时间内
CODE_USER_NOT_EXISTS = 1005     # 用户不存在
CODE_PASSWORD_ERROR = 1006      # 密码不正确
CODE_AUTOORIZATION_ERROR = 1007 # jwt认证失败
CODE_PARAMS_ERROR = 1008        # 参数异常
CODE_NETWORK_ERROR = 1009       # 网络异常

提示语信息: application/utils/message.py,代码:

"""提示文本信息"""

ok = '成功!!'
mobile_format_error = '手机号码格式有误'
mobile_is_use = '手机号已经被注册'
password_not_match = '两次密码输入不一致'
check_data_fail = '数据验证失败'
server_is_error = '服务端程序出错!'
sms_interval_time = '冷却时间内,验证码不能重新发送!'
sms_send_fail = '短信发送失败!'
sms_code_expired = '短信验证码已过期!'
sms_code_not_match = '验证码输入不正确!'
user_not_exists = '用户不存在!'
password_error = '密码不正确!'
authorization_is_invalid = "无效的认证令牌!"
no_authorization = "缺少认证令牌!"
authorization_has_expired = "认证令牌已过期!"
validate_error = '验证码有误!'
params_error = '参数异常!'
network_error = '网络异常!'
posted @ 2021-06-09 09:48  十九分快乐  阅读(105)  评论(0编辑  收藏  举报