Albumy 程序注册时验证令牌不通过,出现 BadSignature 异常

albumy

#1

这是albumy/utils.py文件

import os
import uuid

try:
    from urlparse import urlparse, urljoin
except ImportError:
    from urllib.parse import urlparse, urljoin

import PIL
from PIL import Image
from flask import current_app, request, url_for, redirect, flash
from itsdangerous import BadSignature, SignatureExpired
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

from albumy.extensions import db
from albumy.models import User
from albumy.settings import Operations


def generate_token(user, operation, expire_in=None, **kwargs):
    s = Serializer(current_app.config['SECRET_KEY'], expire_in)

    data = {'id': user.id, 'operation': operation}
    data.update(**kwargs)

    return s.dumps(data)


def validate_token(user, token, operation, new_password=None):
    s = Serializer(current_app.config['SECRET_KEY'])

    try:
        data = s.loads(token)
    except SignatureExpired:
        return False
    except BadSignature:
        return False

    if operation != data.get('operation') or user.id != data.get('id'):
        return False

    if operation == Operations.CONFIRM:
        user.confirmed = True
    elif operation == Operations.RESET_PASSWORD:
        user.set_password(new_password)
    elif operation == Operations.CHANGE_EMAIL:
        new_email = data.get('new_email')
        if new_email is None:
            return False
        if User.query.filter_by(email=new_email).first() is not None:
            return False
        user.email = new_email
    else:
        return False

    db.session.commit()
    return True

下面是修改email地址时的视图函数:
albumy/blueprints/user.py:

@user_bp.route('/settings/change-email', methods=['GET', 'POST'])
@fresh_login_required
def change_email_request():
    form = ChangeEmailForm()
    if form.validate_on_submit():
        token = generate_token(user=current_user, operation=Operations.CHANGE_EMAIL, new_email=form.email.data.lower())
        send_change_email_email(to=form.email.data, user=current_user, token=token)
        flash('Confirm email sent, check your inbox.', 'info')
        return redirect(url_for('.index', username=current_user.username))
    return render_template('user/settings/change_email.html', form=form)


@user_bp.route('/change-email/<token>')
@login_required
def change_email(token):
    if validate_token(user=current_user, token=token, operation=Operations.CHANGE_EMAIL):
        flash('Email updated.', 'success')
        return redirect(url_for('.index', username=current_user.username))
    else:
        flash('Invalid or expired token.', 'warning')
        return redirect(url_for('.change_email_request'))

每次验证,邮件可以发出去,当我点击链接,就提示:Invalid or expired token.

我顺着这个提示,发现验证token时,总是报BadSignature异常。

我又在shell里面进行了验证,如下:

>>> from albumy.utils import generate_token, validate_token
>>> user = User.query.get(1)
>>> operation = 'change-email'
>>> token = generate_token(user=user, operation=operation)
>>> validate_token(user=user, token=token, operation=operation)
False
>>> 

这个问题困扰了我一天,也没有解决,而且之前用户确认时也需要进行token认证,之前是可以的,现在也不成功,而且每次从邮箱里面点击链接,都会再发送一封邮件,不知道什么原因。


#2

shell里面进行了验证,是预期效果

而报BadSignature异常说明token错误了。建议把报错的token也放出来看看。然后,调试信息还是有点不够啊,最好是这样。

def validate_token(user, token, operation, new_password=None):
    s = Serializer(current_app.config['SECRET_KEY'])

    try:
        current_app.logger.debug(e)  # 加这条
        data = s.loads(token)
    except SignatureExpired as e:
        current_app.logger.error(e)  # 加这条
        return False
    except BadSignature as e:
        current_app.logger.error(e)  # 加这条
        return False

    if operation != data.get('operation') or user.id != data.get('id'):
        current_app.logger.error(e)  # 加这条
        return False

    if operation == Operations.CONFIRM:
        user.confirmed = True
    elif operation == Operations.RESET_PASSWORD:
        user.set_password(new_password)
    elif operation == Operations.CHANGE_EMAIL:
        new_email = data.get('new_email')
        if new_email is None:
            current_app.logger.error('new_email is None')  # 加这条
            return False
        if User.query.filter_by(email=new_email).first() is not None:
            current_app.logger.error('new_email error')  # 加这条
            return False
        user.email = new_email
    else:
        current_app.logger.error('operation error')  # 加这条
        return False

    db.session.commit()
    current_app.logger.info('validate_token success')  # 加这条
    return True

#3

按照您的提示,我在validate_token方法里面增加了一些log,大体是这样的:

def validate_token(user, token, operation, new_password=None):
    s = Serializer(current_app.config['SECRET_KEY'])

    try:
        data = s.loads(token)
    except SignatureExpired as e:
        current_app.logger.debug(e)     # log
        return False
    except BadSignature as e:
        current_app.logger.debug(e)    # log
        return False

    if operation != data.get('operation') or user.id != data.get('id'):
        return False

    if operation == Operations.CONFIRM:
        user.confirmed = True
    elif operation == Operations.RESET_PASSWORD:
        user.set_password(new_password)
    elif operation == Operations.CHANGE_EMAIL:
        new_email = data.get('new_email')
        if new_email is None:
            current_app.logger.error('new_email is None')    # log
            return False
        if User.query.filter_by(email=new_email).first() is not None:
            current_app.logger.error('new_email error')    # log
            return False
        user.email = new_email
    else:
        current_app.logger.error('operation error')    # log
        return False

    db.session.commit()
    current_app.logger.info('validate_token success')    # log
    return True

再次运行之后的log:

[2019-04-22 08:34:56,467] DEBUG in utils: Signature b"82TN7q1jig4oytwAnw-O-7NhxiMwDkllGOyNlinNwSWVZEuO7JD3XFzoN7VsfD4pSvkR61o5skC3xuL8UFGj8w'" does not match
192.168.208.1 - - [22/Apr/2019 08:34:56] "GET /user/change-email/b%27eyJhbGciOiJIUzUxMiIsImlhdCI6MTU1NTg5MzIyNywiZXhwIjoxNTU1ODk2ODI3fQ.eyJpZCI6MSwib3BlcmF0aW9uIjoiY2hhbmdlLWVtYWlsIiwibmV3X2VtYWlsIjoieXViYW95aW5nQGdtYWlsLmNvbSJ9.82TN7q1jig4oytwAnw-O-7NhxiMwDkllGOyNlinNwSWVZEuO7JD3XFzoN7VsfD4pSvkR61o5skC3xuL8UFGj8w%27 HTTP/1.1" 302 -
192.168.208.1 - - [22/Apr/2019 08:34:56] "GET /user/settings/change-email HTTP/1.1" 200 -

就是这样,总是显示token后面一段不匹配,不知道什么原因。


#4

现在推进了一步,我在shell里面验证,是可以通过验证的:

>>> token = generate_token(user, operation, new_email='myemail@gmail.com')
>>> validate_token(user, token, operation)
[2019-04-22 08:44:06,364] INFO in utils: validate_token success
True

#5

懂了!我估计你得检查一下你发得邮件正文,URL后面有一点?

[2019-04-22 08:34:56,467] DEBUG in utils: Signature b"82TN7q1jig4oytwAnw-O-7NhxiMwDkllGOyNlinNwSWVZEuO7JD3XFzoN7VsfD4pSvkR61o5skC3xuL8UFGj8w'" does not match

中的

b"82TN7q1jig4oytwAnw-O-7NhxiMwDkllGOyNlinNwSWVZEuO7JD3XFzoN7VsfD4pSvkR61o5skC3xuL8UFGj8w'" 

最后那个'是不是有问题?总之检测传参,还有字符串拼接的地方。


#6

这个是新版本 Werkzeug 带来的问题,生成 URL 会把 bytes 字符串的 b 前缀和引号加入到 URL 中,把 Werkzeug 版本回退到 0.14.1 即可解决(建议安装依赖时从示例程序自带的 Pipfile.lock 安装)。

相关 issue:https://github.com/pallets/werkzeug/issues/1502


#7

下次发帖请在标题内包含必要的关键信息,并正确设置分类和标签。详情见 技术提问帖发帖规则


#8

好的。
问题解决了。
谢谢两位。
:+1:
:pray:
:muscle:


#9

备注一下,这个 bug 已在 master 分支修复( https://github.com/pallets/werkzeug/pull/1522 )。