前言

原本使用的是session,认证,服务器会把session-id存在浏览器的cookie里面,然后在服务端会保存session-id,也看到很多flask关于session的操作,自定义保存到redis等等,但是在负载均衡的时候,就会发现,当服务器多起来之后,访问的时候其他的服务器并没有保存另一个服务器产生的session-id。当然保存到同一个数据库可以解决,但是我试了一下使用token来认证。
大概思路就是借鉴了这个视频的,可能有很多不符合规范,还请大家多多指出


apifox介绍token的使用


思路

一般token都是放在请求头的Authorization,但是我还是个萌新,项目是使用服务器渲染的,没有做前后端分离,好像没法设置请求头,所以我就观察了一下请求头,发现cookie是每次都会发过来的,所以我就将token存在cookie里面,每次发过来,进行验证,如果能够每次通过验证,就能够保持登录态。

用到的东西

  • flask的钩子函数
  • pyjwt模块

具体实现

token的生成验证

1. 生成token

这个传入的user参数是ORM对象,就是数据库的单个用户的类,具体是这样的:user = UserModel.query.filter(UserModel.id ==user_id).first() 设置好headers和payload之后,直接使用jwt.encode就可以了,就能得到一个加密的字符串了

def create_token(user):
    headers = {
        "alg": "HS256",
        "typ": "JWT",
    }
    payload = {
        "name": user.username,
        "role": user.role,
        "user_id": user.id,
        "exp": int(time.time() + 3600),
        "iss": 'cgy'
    }
    token = jwt.encode(payload=payload, key=current_app.config.get('SECRET_KEY'), algorithm='HS256',
                       headers=headers)
    return token

2. 验证token

底下可以发现jwt.decode将token解密,可以得到上面写的payload,并且验证issuer。如果验证失败,返回msg,即变量msg不为None,之后也可以根据payload和msg是否为None,进行判断验证是否成功。

def validate_token(token):
    payload = None
    msg = None
    try:
        payload = jwt.decode(jwt=token, key=current_app.config.get('SECRET_KEY'), algorithms=['HS256'], issuer='cgy')
    except exceptions.ExpiredSignatureError:
        msg = 'token已失效'
    except jwt.DecodeError:
        msg = 'token认证失败'
    except jwt.InvalidTokenError:
        msg = '非法的token'
    return payload, msg

用户认证流程

在每次接收请求的时候,提取出cookie中的token,然后在每次请求操作结束的时候,生成token,放到cookie中

接收请求时

从cookie中取出token,然后进行认证,如果

@app.before_request
def before_request():
    payload, msg = validate_token(request.cookies.get('token'))
    if payload is not None and msg is None:
        user = UserModel.query.filter(UserModel.id == token.get('user_id')).first()
        g.user = user

结束请求时

这个resp是一个响应对象,可以在视图函数中给他绑定一些属性,在这里能够识别到

  • 首先是查看resp这个对象有没有logout这个属性,并且这个属性的值为True,如果是的话,就删除cookie
  • 如果不是退出登录的话,就查看g对象或者resp对象,有没有user这个属性,如果有的话就生成token并且存到cookie中
  • g对象:可以看到上面的接收请求的时候,会认证一次,如果能够通过的话,将user存到g对象中,所以这里查看g对象中是否有user这个属性是在检查,这是不是一个登录过的用户的发送请求
@app.after_request
def after_request(resp):
    if hasattr(resp, 'logout') and resp.logout is True:
        resp.delete_cookie('token')
    else:
        if hasattr(g, 'user'):
            token = create_token(g.user)
            resp.set_cookie('token', token, max_age=3600)
    return resp

登录赋予登录态

我们会发现一个问题,在接受请求的时候,是先获取到了cookie再给g对象绑定user,这里结束请求的时候再通过在检查g对象中有没有user。我们好像没有在哪里设置g对象有user,从而设置cookie,那么这个所谓的第一次,就在login视图函数里面,可以看到,当使用post请求时,有g.user = user,这个第一次,给g对象绑定user,从而结束请求时开始将token加入到cookie中

@bp.route('/login/', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        if hasattr(g, 'user'):
            return redirect('/')
        return render_template('login.html')
    elif request.method == 'POST':
        form = LoginForm(request.form)
        if form.validate():
            user = UserModel.query.filter(UserModel.username == form.username.data).first()
            g.user = user
            flash('登录成功')
            return redirect(url_for('food.index'))
        else:
            flash('登录失败')
            return render_template('login.html')

总结

笼统来看就是,
登录的时候,给g对象绑定user,然后在结束那一次请求的时候,会把token加入到cookie中。
在之后接收请求时,会获取cookie中的token并且认证一下,知道这是登录后的用户,再给g对象绑定user
在返回响应前,查看g对象是否有user属性,如果有的话,再更新cookie(因为有时效)