本教程的知识点为:简介 1. 内容 2. 目标 产品效果 ToutiaoWeb虚拟机使用说明 数据库 理解ORM 作用 思考: 使用ORM的方式选择 数据库 SQLAlchemy操作 1 新增 2 查询 all() 数据库 分布式ID 1 方案选择 2 头条 使用雪花算法 (代码 toutiao-backend/common/utils/snowflake) 数据库 Redis 1 Redis事务 基本事务指令 Python客户端操作 Git工用流 调试方法 JWT认证方案 JWT & JWS & JWE Json Web Token(JWT) OSS对象存储 存储 需求 方案 使用 缓存 缓存架构 多级缓存 头条项目的方案 缓存数据 缓存 缓存问题 1 缓存 2 缓存 头条项目缓存与存储设计 APScheduler定时任务 定时修正统计数据 RPC RPC简介 1. 什么是RPC RPC 编写客户端 头条首页新闻推荐接口编写 即时通讯 即时通讯简介 即时通讯 Socket.IO 1 简介 优点: 缺点: Elasticsearch 简介与原理 1 简介 属于面向文档的数据库 2 搜索的原理——倒排索引(反向索引)、分析、相关性排序 Elasticsearch 文档 索引文档(保存文档数据) 获取指定文档 判断文档是否存在 单元测试 为什么要测试 测试的分类 什么是单元测试 断言方法的使用:

完整笔记资料代码:https://gitee.com/yinuo112/Backend/tree/master/Python/嘿马头条项目从到完整开发教程/note.md

感兴趣的小伙伴可以自取哦~


全套教程部分目录:


部分文件图片:

Git工用流

调试方法

  1. 判断问题发生在前端还是后端
    • 如果前端为网页,可以通过网页调试工具里面的network判断

      • 在前端中触发一次接口调用的请求
      • 查看network中对应的请求记录
      • 如果没有发现有新的请求记录,表示前端有问题,并未发起请求
      • 如果有请求记录,返回的状态码为200,但是页面没有想要的效果,表示前端没有正确处理200的响应
      • 如果返回的状态码为4xx, 表示前端构造的请求有问题,比如404没有正确的请求网址,401、403请求的身份有问题,405请求的方式有问题,400表示构造的请求参数不正确(类型不对或缺少参数)
      • 如果返回500状态码,表示服务器端出现问题
    • 如果前端不是网页,比如app,通过日志的访问请求记录判断

    在服务器中查看日志文件的方法:

tail flask.log # 查看最后一条记录 tail -n 100 flask.log # 查看最新的100条记录 tail -f flask.log # 实时查看





2. ##### 如果是后端出现的问题,通过pycharm或日志来判断


* 查看记录错误的日志,根据日志的信息判断错误





# JWT认证方案







# JWT & JWS & JWE







### Json Web Token(JWT)




JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在两个组织之间传递安全可靠的信息。
> 
官方定义:JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties

![img](https://s2.51cto.com/images/blog/front/202408/d5f8137067adf234b0458613a2b1908ff852a2.png)

现在网上大多数介绍JWT的文章实际介绍的都是JWS(JSON Web Signature),也往往导致了人们对于JWT的误解,但是JWT并不等于JWS,JWS只是JWT的一种实现,除了JWS外,JWE(JSON Web Encryption)也是JWT的一种实现。

下面就来详细介绍一下JWT与JWE的两种实现方式:

![img](https://s2.51cto.com/images/blog/front/202408/735eebb7030a133953a3242d7353131f9133d7.png)




### JSON Web Signature(JWS)




JSON Web Signature是一个有着简单的统一表达形式的字符串:

![img](https://s2.51cto.com/images/blog/front/202408/01d941a57b86fd7c1ab472654e4a201ffe534b.png)




##### 头部(Header)




头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。 JSON内容要经Base64 编码生成字符串成为Header。




##### 载荷(PayLoad)




payload的五个字段都是由JWT的标准所定义的。

1. iss: 该JWT的签发者
2. sub: 该JWT所面向的用户
3. aud: 接收该JWT的一方
4. **exp(expires): 什么时候过期,这里是一个Unix时间戳**
5. iat(issued at): 在什么时候签发的

后面的信息可以按需补充。 JSON内容要经Base64 编码生成字符串成为PayLoad。




##### 签名(signature)




这个部分header与payload通过header中声明的加密方式,使用密钥secret进行加密,生成签名。 JWS的主要目的是保证了数据在传输过程中不被修改,验证数据的完整性。但由于仅采用Base64对消息内容编码,因此不保证数据的不可泄露性。所以不适合用于传输敏感数据。




### JSON Web Encryption(JWE)




相对于JWS,JWE则同时保证了安全性与数据完整性。 JWE由五部分组成:

![img](https://s2.51cto.com/images/blog/front/202408/f880b3b678b917693e4346e372807dd492525f.png)

JWE组成

具体生成步骤为:

1. JOSE含义与JWS头部相同。
2. 生成一个随机的Content Encryption Key (CEK)。
3. 使用RSAES-OAEP 加密算法,用公钥加密CEK,生成JWE Encrypted Key。
4. 生成JWE初始化向量。
5. 使用AES GCM加密算法对明文部分进行加密生成密文Ciphertext,算法会随之生成一个128位的认证标记Authentication Tag。 6.对五个部分分别进行base64编码。

可见,JWE的计算过程相对繁琐,不够轻量级,因此适合与数据传输而非token认证,但该协议也足够安全可靠,用简短字符串描述了传输内容,兼顾数据的安全性与完整性。




# JWT的Python库







### 独立的JWT Python库




* itsdangerous

* JSONWebSignatureSerializer
* TimedJSONWebSignatureSerializer (可设置有效期)

* pyjwt

[




### 安装




```shell
$ pip install pyjwt

用例

>>> import jwt

  >>> encoded_jwt = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
  >>> encoded_jwt
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'

  >>> jwt.decode(encoded_jwt, 'secret', algorithms=['HS256'])
  {'some': 'payload'}

头条项目封装

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

头条项目实施方案

需求

设置有效期,但有效期不宜过长,需要刷新。

如何解决刷新问题?

  • 手机号+验证码(或帐号+密码)验证后颁发接口调用token与refresh_token(刷新token)

  • Token 有效期为2小时,在调用接口时携带,每2小时刷新一次

  • 提供refresh_token,refresh_token 有效期14天

  • 在接口调用token过期后凭借refresh_token 获取新token

  • 未携带token 、错误的token或接口调用token过期,返回401状态码

  • refresh_token 过期返回403状态码,前端在使用refresh_token请求新token时遇到403状态码则进入用户登录界面从新认证。

  • token的携带方式是在请求头中使用如下格式:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg

注意:Bearer前缀与token中间有一个空格

实现
注册或登录获取token

toutiao/resources/user/passport.pty

class AuthorizationResource(Resource):
    """
    认证
    """
    def _generate_tokens(self, user_id, with_refresh_token=True):
        """
        生成token 和refresh_token
        :param user_id: 用户id
        :return: token, refresh_token
        """
         # 颁发JWT
        now = datetime.utcnow()
        expiry = now + timedelta(hours=current_app.config['JWT_EXPIRY_HOURS'])
        token = generate_jwt({'user_id': user_id, 'refresh': False}, expiry)
        refresh_token = None
        if with_refresh_token:
            refresh_expiry = now + timedelta(days=current_app.config['JWT_REFRESH_DAYS'])
            refresh_token = generate_jwt({'user_id': user_id, 'refresh': True}, refresh_expiry)
        return token, refresh_token

    def post(self):
        """
        登录创建token
        """
        json_parser = RequestParser()
        json_parser.add_argument('mobile', type=parser.mobile, required=True, location='json')
        json_parser.add_argument('code', type=parser.regex(r'^\d{6}$'), required=True, location='json')
        args = json_parser.parse_args()
        mobile = args.mobile
        code = args.code

        # 从redis中获取验证码
        key = 'app:code:{}'.format(mobile)
        try:
            real_code = current_app.redis_master.get(key)
        except ConnectionError as e:
            current_app.logger.error(e)
            real_code = current_app.redis_slave.get(key)

        try:
            current_app.redis_master.delete(key)
        except ConnectionError as e:
            current_app.logger.error(e)

        if not real_code or real_code.decode() != code:
            return {'message': 'Invalid code.'}, 400

        # 查询或保存用户
        user = User.query.filter_by(mobile=mobile).first()

        if user is None:
            # 用户不存在,注册用户
            user_id = current_app.id_worker.get_id()
            user = User(id=user_id, mobile=mobile, name=mobile, last_login=datetime.now())
            db.session.add(user)
            profile = UserProfile(id=user.id)
            db.session.add(profile)
            db.session.commit()
        else:
            if user.status == User.STATUS.DISABLE:
                return {'message': 'Invalid user.'}, 403

        token, refresh_token = self._generate_tokens(user.id)

        return {'token': token, 'refresh_token': refresh_token}, 201
请求钩子

common/utils/middlewares.py

from flask import request, g
from .jwt_util import verify_jwt

def jwt_authentication():
    """
    根据jwt验证用户身份
    """
    g.user_id = None
    g.is_refresh_token = False
    authorization = request.headers.get('Authorization')
    if authorization and authorization.startswith('Bearer '):
        token = authorization.strip()[7:]
        payload = verify_jwt(token)
        if payload:
            g.user_id = payload.get('user_id')
            g.is_refresh_token = payload.get('refresh')
强制登录装饰器

common/utils/decorators.py

def login_required(func):
    """
    用户必须登录装饰器
    使用方法:放在method_decorators中
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not g.user_id:
            return {'message': 'User must be authorized.'}, 401
        elif g.is_refresh_token:
            return {'message': 'Do not use refresh token.'}, 403
        else:
            return func(*args, **kwargs)

    return wrapper
更新token接口

toutiao/resources/user/passport.py

class AuthorizationResource(Resource):
    """
    认证
    """

    ...

    # 补充put方式 更新token接口
    def put(self):
        """
        刷新token
        """
        user_id = g.user_id
        if user_id and g.is_refresh_token:
            token, refresh_token = self._generate_tokens(user_id, with_refresh_token=False)
            return {'token': token}, 201
        else:
            return {'message': 'Wrong refresh token.'}, 403

JWT禁用问题

需求

token颁发给用户后,在有效期内服务端都会认可,但是如果在token的有效期内需要让token失效,该怎么办?

此问题的应用场景:

  • 用户修改密码,需要颁发新的token,禁用还在有效期内的老token
  • 后台封禁用户
解决方案

在redis中使用set类型保存新生成的token

key = 'user:{}:token'.format(user_id)
pl = redis_client.pipeline()
pl.sadd(key, new_token)
pl.expire(key, token有效期)
pl.execute()
类型
user:{user_id}:token set 新token

客户端使用token进行请求时,如果验证token通过,则从redis中判断是否存在该用户的user:{}:token记录:

  • 若不存在记录,放行,进入视图进行业务处理

  • 若存在,则对比本次请求的token是否在redis保存的set中:

    • 若存在,则放行
    • 若不在set的数值中,则返回403状态码,不再处理业务逻辑
key = 'user:{}:token'.format(user_id)
valid_tokens = redis_client.smembers(key, token)
if valid_tokens and token not in valid_tokens:
  return {'message': 'Invalid token'.}, 403
说明:
  1. redis记录设置有效期的时长是一个token的有效期,保证旧token过期后,redis的记录也能自动清除,不占用空间。
  2. 使用set保存新token的原因是,考虑到用户可能在旧token的有效期内,在其他多个设备进行了登录,需要生成多个新token,这些新token都要保存下来,既保证新token都能正常登录,又能保证旧token被禁用

OSS对象存储