主要内容
- 获取验证码
- 注册登录
- 获取当前用户信息
- 查看前端交互
原型效果
接口设计
# 获取短信验证码
/app/sms/codes/<mobile>
# 请求方式
GET
# 请求参数 路径参数
mobile 手机号
响应数据 json
{
"message": "ok",
"data": {
"mobile": 135xxxxxxxx
}
}
接口实现
- 在 app/resource/user/passport文件中实现获取验证码视图函数
# app/resources/user/passport.py
from flask_restful import Resource
import random
from app import redis_client
from utils.constants import SMS_CODE_EXPIRE
class SMSCodeResource(Resource):
"""获取短信验证码"""
def get(self, mobile):
# 生成短信验证码
rand_num = '%06d' % random.randint(0, 999999)
# 保存验证码(redis) app:code:18912341234 123456
key = 'app:code:{}'.format(mobile)
redis_client.set(key, rand_num, ex=SMS_CODE_EXPIRE)
# 发送短信 第三方短信平台 celery
print('短信验证码: "mobile": {}, "code": {}'.format(mobile, rand_num))
# 返回结果
return {'mobile': mobile}
- 注意: 短信验证码的过期时间需要定义为常量, 在 common/utils/constants.py文件中设置
# common/utils/constants.py
...
SMS_CODE_EXPIRE = 300 # 短信验证码有效期
配置URL
- 在 user包的初始化文件中设置类视图, 给类视图的URL添加路径参数
# app/resources/user/__init__.py
...
user_api.add_resource(SMSCodeResource, '/sms/codes/<mobile>')
- 在 common/utils/constants文件中定义常量记录URL前缀, 并在user包的初始化文件中给蓝图设置统一的资源段前缀
# common/utils/constants.py
...
BASE_URL_PRIFIX = '/app' # 基础URL的前缀
# app/resources/user/__init__.py
...
from utils.constants import BASE_URL_PRIFIX
...
user_bp = Blueprint('user', __name__, url_prefix=BASE_URL_PRIFIX)
配置路由转化器
- 在 common/utils包中导入物料converters.py, 其中包含了常见的路由转换器, 以便对路由变量进行参数校验
# common/utils/converters.py
from werkzeug.routing import BaseConverter
class MobileConverter(BaseConverter):
"""
手机号格式
"""
regex = r'1[3-9]\d{9}'
def register_converters(app):
"""
向Flask app中添加转换器
:param app: Flask app对象
"""
app.url_map.converters['mob'] = MobileConverter
在 app包的初始化文件中注册路由转换器
# app/__init__.py
def register_extensions(app):
"""组件初始化"""
...
# 添加转换器
from utils.converters import register_converters
register_converters(app)
在 user包的初始化文件中, 给类视图的路径参数添加转换器
# app/resources/user/__init__.py
...
user_api.add_resource(SMSCodeResource, '/sms/codes/<mob:mobile>')
注册登录
# 注册登录
/app/authorizations
# 请求方式
POST
# 请求参数 json
mobile 手机号
code 短信验证码
响应数据 json
{
"message": "ok",
"data": {
"token": "xxxxxxxx"
}
}
模型设计
- SQLAlchemy-用户模型类
在 common包中创建 models包, 用于存放各类模型数据
在 common/models包中添加物料 user.py文件, 其中包含了用户模型类
# common/models/user.py
from app import db
class User(db.Model):
"""
用户基本信息
"""
__tablename__ = 'user_basic'
id = db.Column(db.Integer, primary_key=True, doc='用户ID')
mobile = db.Column(db.String(11), doc='手机号')
name = db.Column(db.String(20), doc='昵称')
last_login = db.Column(db.DateTime, doc='最后登录时间')
introduction = db.Column(db.String(50), doc='简介')
article_count = db.Column(db.Integer, default=0, doc='作品数')
following_count = db.Column(db.Integer, default=0, doc='关注的人数')
fans_count = db.Column(db.Integer, default=0, doc='粉丝数')
profile_photo = db.Column(db.String(130), doc='头像')
def to_dict(self):
"""模型转字典, 用于序列化处理"""
return {
'id': self.id,
'name': self.name,
'photo': self.profile_photo,
'intro': self.introduction,
'art_count': self.article_count,
'follow_count': self.following_count,
'fans_count': self.fans_count
}
数据迁移
在 app包的初始化文件的 register_extensions函数中, 对数据迁移组件进行初始化
# app/__init__.py
...
from flask_migrate import Migrate
...
def register_extensions(app):
"""组件初始化"""
...
# 数据迁移组件初始化
Migrate(app, db)
# 导入模型类
from models import user
执行数据迁移命令
export FLASK_APP=app.main # 设置环境变量指定启动文件
flask db init # 生成迁移文件夹
flask db migrate # ⽣成迁移版本, 保存到迁移文件夹中
flask db upgrade # 执行迁移
- 由于数据迁移代码是自动生成的, 为了避免代码合并时出现冲突, 一般不会让git管理迁移文件夹
- 将迁移文件夹设置到gitignore文件中进行忽略处理
# .gitignore
*.py[cod]
.idea
migration
配置请求校验函数
- 在 common/utils包中导入物料parser.py, 其中包含了常见的请求解析器使用的自定义校验函数, 以便对请求数据进行参数解析
# common/utils/parser.py
import re
import base64
import imghdr
from datetime import datetime
def email(email_str):
"""
检验邮箱格式
:param email_str: str 被检验字符串
:return: email_str
"""
if re.match(r'^([A-Za-z0-9_\-\.\u4e00-\u9fa5])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,8})$', email_str):
return email_str
else:
raise ValueError('{} is not a valid email'.format(email_str))
def mobile(mobile_str):
"""
检验手机号格式
:param mobile_str: str 被检验字符串
:return: mobile_str
"""
if re.match(r'^1[3-9]\d{9}$', mobile_str):
return mobile_str
else:
raise ValueError('{} is not a valid mobile'.format(mobile_str))
def id_number(value):
"""检查是否为身份证号"""
id_number_pattern = r'(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)'
if re.match(id_number_pattern, value):
return value.upper()
else:
raise ValueError('Invalid id number.')
接口实现
- 在 app/resource/user/passport文件中实现注册登录视图函数
# app/resources/user/passport.py
...
from datetime import datetime, timedelta
from flask import current_app
from flask_restful.inputs import regex
from flask_restful.reqparse import RequestParser
from sqlalchemy.orm import load_only
from app import db
from utils.parser import mobile as mobile_type
from models.user import User
...
class LoginResource(Resource):
"""注册登录"""
def post(self):
# 获取参数
parser = RequestParser()
parser.add_argument('mobile', required=True, location='json', type=mobile_type)
parser.add_argument('code', required=True, location='json', type=regex(r'^\d{6}$'))
args = parser.parse_args()
mobile = args.mobile
code = args.code
# 校验短信验证码
key = 'app:code:{}'.format(mobile)
real_code = redis_client.get(key)
if not real_code or real_code != code:
return {'message': 'Invalid Code', 'data': None}, 400
# 删除验证码
# redis_client.delete(key)
# 校验成功, 查询数据库
user = User.query.options(load_only(User.id)).filter(User.mobile == mobile).first()
if user: # 如果有, 取出用户id, 更新最后登录时间
user.last_login = datetime.now()
else: # 如果没有, 创建新用户
user = User(mobile=mobile, name=mobile, last_login=datetime.now())
db.session.add(user)
db.session.commit()
# 返回结果
return {'userid': user.id}, 201
注意点
- 使用 load_only等语法, 尽量只查询目标字段, 提高查询效率
配置URL
在 user包的初始化文件中设置类视图的URL
# app/resources/user/__init__.py
from .passport import SMSCodeResource, LoginResource
# 添加类视图
user_api.add_resource(LoginResource, '/authorizations')
状态保持
PyJWT
- 注册登录后需要对用户信息进行状态保持, 可选方案包括: Cookie Session JWT
- 相比HTTP自带的状态保持机制, JWT的优点
移动端不支持状态保持机制
cookie有同源策略, 默认无法跨站传输 (nginx可以转发) - python中普遍使用的jwt拓展包 pip install pyjwt
生成JWT需要设置秘钥, 可以使用随机字符串
base64.b64encode(os.urandom(40)).decode()
配置JWT工具函数
- 在 common/utils包中导入物料jwt_util.py, 其中包含了使用 PYJWT 封装的JWT的生成和校验函数
# common/utils/jwt_util.py
import jwt
from flask import current_app
def generate_jwt(payload, expiry, secret=None):
"""
生成jwt
:param payload: dict 载荷
:param expiry: datetime 有效期
:param secret: 密钥
:return: jwt
"""
_payload = {'exp': expiry}
_payload.update(payload)
if not secret:
secret = current_app.config['JWT_SECRET']
token = jwt.encode(_payload, secret, algorithm='HS256')
return token.decode()
def verify_jwt(token, secret=None):
"""
检验jwt
:param token: jwt
:param secret: 密钥
:return: dict: payload
"""
if not secret:
secret = current_app.config['JWT_SECRET']
try:
payload = jwt.decode(token, secret, algorithm=['HS256'])
except jwt.PyJWTError:
payload = None
return payload
- 注意: JWT的秘钥和默认过期时间封装成了应用配置, 在 app/settings/config.py文件中设置
# app/settings/config.py
class DefaultConfig:
"""默认配置"""
...
# JWT
JWT_SECRET = 'TPmi4aLWRbyVq8zu9v82dWYW17/z+UvRnYTt4P6fAXA' # 秘钥
JWT_EXPIRE_DAYS = 14 # JWT过期时间14天
接口实现
- 在 app/resource/user/passport文件的注册登录视图函数中生成jwt
# app/resources/user/passport.py
...
from utils.jwt_util import generate_jwt
...
class LoginResource(Resource):
"""注册登录"""
def post(self):
...
db.session.commit()
# 生成jwt
token = generate_jwt({'userid': user.id}, expiry=datetime.utcnow() + timedelta(days=current_app.config['JWT_EXPIRE_DAYS']))
# 返回结果
return {'token': token}, 201
获取用户信息
接口设计
# 获取当前用户信息
/app/user
# 请求方式
GET
# 请求头
Authorization 用户token
响应数据 json
{
"message": "OK",
"data": {
"id": 1155,
"name": "18912341234",
"photo": "xxxxx",
"intro": "xxx",
"art_count": 0,
"follow_count": 0,
"fans_count": 0
}
}
相关模型类
- SQLAlchemy-用户模型类
# common/models/user.py
from app import db
class User(db.Model):
"""
用户基本信息
"""
__tablename__ = 'user_basic'
id = db.Column(db.Integer, primary_key=True, doc='用户ID')
mobile = db.Column(db.String(11), doc='手机号')
name = db.Column(db.String(20), doc='昵称')
last_login = db.Column(db.DateTime, doc='最后登录时间')
introduction = db.Column(db.String(50), doc='简介')
article_count = db.Column(db.Integer, default=0, doc='作品数')
following_count = db.Column(db.Integer, default=0, doc='关注的人数')
fans_count = db.Column(db.Integer, default=0, doc='粉丝数')
profile_photo = db.Column(db.String(130), doc='头像')
def to_dict(self):
"""模型转字典, 用于序列化处理"""
return {
'id': self.id,
'name': self.name,
'photo': self.profile_photo,
'intro': self.introduction,
'art_count': self.article_count,
'follow_count': self.following_count,
'fans_count': self.fans_count
}
代码实现
实现权限控制
获取用户信息接口有访问权限要求: 用户登录才能访问, 所以需要实现权限控制, 需要实现以下两步:
定义钩子函数: 获取用户信息, 并使用g变量传递数据
定义装饰器: 根据用户信息进行访问限制
- 在 common/utils包中新建middlewares.py, 其中定义钩子函数, 用于获取用户信息
# common/utils/middlewares.py
from flask import request, g
from utils.jwt_util import verify_jwt
def get_userinfo():
"""获取用户信息"""
# 获取请求头中的token
token = request.headers.get('Authorization')
g.userid = None # 如果未登录, userid=None
if token: # 如果传递了token
# 校验token
data = verify_jwt(token)
if data: # 校验成功
g.userid = data.get('userid') # 如果已登录, userid=11
- 在 app包的初始化文件中注册钩子函数
# app/__init__.py
def register_extensions(app):
"""组件初始化"""
...
# 添加请求钩子
from utils.middlewares import get_userinfo
app.before_request(get_userinfo)
- 在 common/utils包中新建decorators.py, 其中定义装饰器, 用于访问限制
# common/utils/decorators.py
from flask import g
from functools import wraps
def login_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
# 如果用户已登录, 正常访问
if g.userid:
return f(*args, **kwargs)
else:
return {'message': 'Invalid Token', 'data': None}, 401
return wrapper
接口实现
- 在 app/resource/user包中新建 profile.py文件, 并在其中实现获取用户信息视图函数
# app/resources/user/profile.py
from flask import g
from flask_restful import Resource
from sqlalchemy.orm import load_only
from models.user import User
from utils.decorators import login_required
class CurrentUserResource(Resource):
"""个人中心-当前用户"""
method_decorators = {'get': [login_required]}
def get(self):
# 获取用户id
userid = g.userid
# 查询用户数据
user = User.query.options(load_only(User.id, User.name, User.profile_photo, User.introduction, User.article_count, User.following_count, User.fans_count)).filter(User.id == userid).first()
return user.to_dict()
配置URL
- 在 user包的初始化文件中设置类视图的URL
# app/resources/user/__init__.py
from .profile import CurrentUserResource
# 添加类视图
user_api.add_resource(CurrentUserResource, '/user')