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用户登录认证。
注意:
flask_jwt_extended
的作者开发当前模块主要适用于flask的普通视图方法的。其认证方式主要通过装饰器来完成。而我们当前所有服务端接口都改造成了jsonrpc规范接口,所以我们在使用过程中,需要对部分源代码进行调整才能正常使用。- 事实上,在我们当前使用的
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/
初始化配置
- 在魔方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
- 开发环境配置,
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。
- 视图:
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'
- 数据库操作:
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
- 路由:
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),
]
- 自定义状态码
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 # 密码不正确
- 文本提示信息
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处的源码调整,以方便它更好的展示错误信息。
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
- 自定义响应状态码
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认证失败
- 文本提示信息
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 = "认证令牌已过期!"
记录用户登录信息
- 视图
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'
- 数据服务层,
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() # 提交保存到数据库
- 获取地理位置信息
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 {}
- 开发环境配置
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",
]
客户端提交登录信息
- 发送登陆请求
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'
});
- 封装对于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://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
}
}
// ....
}
客户端展示验证码
- 登陆页面添加防水墙验证码 :
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>
- 在前端用户完成验证码的校验工作以后,调整登陆方法,然后附带校验结果到服务端。
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>
服务端登陆接口中校验验证码回调是否正确
- 封装了一个验证码验证工具函数,
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
- 视图调用验证码验证工具函数
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'
- 错误提示码和提示语信息
错误提示码 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 = '网络异常!'