原代码:
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id}).decode('utf-8')
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True
对应界面,点击这里跳转报错
报错(只截取了部分):
File "E:\program\Python\Web\MySecondFlaskApp\app\auth\views.py", line 99, in resend_confirmation
token = current_user.generate_confirmation_token()
File "E:\program\Python\Web\MySecondFlaskApp\app\models.py", line 44, in generate_confirmation_token
return s.dumps({'confirm': self.id}).decode('utf-8')
File "E:\program\Python\Web\MySecondFlaskApp\venv\lib\site-packages\itsdangerous\serializer.py", line 208, in dumps
rv = self.make_signer(salt).sign(payload)
File "E:\program\Python\Web\MySecondFlaskApp\venv\lib\site-packages\itsdangerous\timed.py", line 55, in sign
return value + sep + self.get_signature(value)
File "E:\program\Python\Web\MySecondFlaskApp\venv\lib\site-packages\itsdangerous\signer.py", line 209, in get_signature
key = self.derive_key()
File "E:\program\Python\Web\MySecondFlaskApp\venv\lib\site-packages\itsdangerous\signer.py", line 195, in derive_key
bytes, self.digest_method(self.salt + b"signer" + secret_key).digest()
TypeError: unsupported operand type(s) for +: 'int' and 'bytes'
- 尝试1:
看报错的倒数第二行,self.digest_method(self.salt + b"signer" + secret_key).digest() 三个参数相加,以为是其中有参数类型变成int类型了,salt,signer都不是我指定的,那问题就出在secret_key。于是跑到配置文件把SECRET_KEY = 'hard to guess string’改成b ‘hard to guess string’ ,果不其然,失败 - 尝试2:
看s.dumps的源码,并且结合这两篇文章浅浅理解了一下源码
文章1 和 文章2 大致猜想是参数编码的问题,编码导致数据类型出错。查资料得知:
str通过encode()方法可以编码为指定的bytes。反过来,当从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法。反之,则使用encode()方法即可!
- 怪不得代码里对token先decode 再encode的,那我把编码删掉,或者调换一下,先encode再decode… 果不其然,都失败
忽然想起来,前面跟着狗书导的包from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
报错
ImportError: cannot import name 'TimedJSONWebSignatureSerializer' from 'itsdangerous
一查原来是我安装的 itsdangerous的版本(2.1.2)已经摒弃了这个类,在Stack Overflow上查到的解决办法是,from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
。其他代码没变。
是不是这个包有问题,毕竟换了一个包,代码可能需要稍微修改。
打开看看这个包的源码,从url_safe.py 中找到了父类Serializer,进入父类包serializer.py 找到,看看各个参数的类型。等等,我肯定是哪里传参传错了,才会导致int和bytes类型相加。但是是哪里呢?不知道
耗费了一上午的时间,有点想走捷径:降低itsdangerous的版本,完全跟着书上走算了
但是好像答案就在眼前了…
走,去Stack Overflow上看看,虽然不知道怎么描述问题…把最后一行的报错信息粘上去试试吧
这个好像跟我的问题一样!Unsupported operand type(s) when using URLSafeTimedSerializer.dumps from itsdangerous 不应该给传入Serializer类的第二个参数传入int类型expiration=3600,而报错确实是int类型和bytes类型相加…刚才太关注bytes了,忽略了int是从哪里来的
参考答案是把 expiration 这个参数去掉了
但是没有有效期怎么行呢?该在什么地方设置token的有效期呢?
(参考答案还让把 .decode(‘utf-8’)去掉,因为s.dump已经是str了,不需要再解码。如果需要bytes类型,可以相应地执行编码 .encode(‘utf-8’))
继续搜索:the “dumps” method of itsdangerous throws a TypeError expiration根本就不是Serializer的参数,而设置有效期,是在s.loads函数里面,叫max_age
更改代码为:
def generate_confirmation_token(self):
s = Serializer(current_app.config['SECRET_KEY'], 'confirmation')
return s.dumps({'confirm': self.id})
def confirm(self, token, max_age=3600):
s = Serializer(current_app.config['SECRET_KEY'], 'confirmation')
try:
data = s.loads(token.encode('utf-8'),max_age=max_age)
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True
记得相应调整调用这俩函数的地方,尤其比如单元测试里面的:
- 在
test_expired_confirmation_token()
函数中:token = u.generate_confirmation_token(1)
,把括号中的1去掉。另外self.assertFalse(u.confirm(token))
改成self.assertFalse(u.confirm(token,max_age=1))
最后修改model后得迁移一下数据库,再运行,成功啦!:
总结: Stack Overflow真好用 …(×
想解决问题,一个重要因素是怎样准确描述问题。有时候问题不是没有办法解决,而是问错方向了才找不到答案。
但是刚开始,你可能不熟悉这个问题,问不到点子上,会搜寻一些不相关的问题答案,但是在搜寻答案和查看源码的过程中不断修正问的问题,使之更接近bug的本质。最终问出恰当的问题,搜到正确地答案。这个过程是非常重要且不可避免的。我相信很少有人每次遇到bug,都能马上找到答案