Flask Vue.js全栈开发|第5章:个人主页与用户头像
Synopsis: 用户认证修改为 JWT(JSON Web Token),后端使用 pyjwt 库生成 JWT 并验证合法性;前端使用 JSON.parse 解析 JWT 中的 payload 数据。提示消息改用 vue-toasted 插件,方便好用。前端每次访问 API 都需要附带 Token 到 Authorization 请求头中,使用请求拦截器自动添加进去,另外设置了响应拦截器,自动处理 401 和 404 错误。用户头像使用在线的 Gravatar 服务,前端使用 moment.js 格式化时间。个人主页的路由参数变化后默认是重用组件并不会重新加载数据,要使用 vue-router 的 beforeRouteUpdate() 导航守卫
代码已上传到 https://github.com/wangy8961/flask-vuejs-madblog/tree/v0.5 ,欢迎star
1. JWT
上一篇文章通过后端创建 Token 并保存到数据库中(同时记录 Token 的失效时间),结合 Flask-HTTPAuth
插件实现 Basic Auth
和 Token Auth
。但是,前端用户登录后,并不知道自己的用户 id
,就没办法调用 GET /api/users/<id>
来获取自己的用户信息
1.1 pyjwt
现在改用 JWT
实现,它可以在 Token 中添加一些不是隐私的数据 payload
,比如我们可以把用户 id
放进去。后端安装 pyjwt
包:
(venv) D:\python-code\flask-vuejs-madblog\back-end>pip install pyjwt (venv) D:\python-code\flask-vuejs-madblog\back-end>pip freeze > requirements.txt
修改 User 数据模型 back-end/app/models.py
,删除之前的 token
和 token_expiration
字段:
class User(PaginatedAPIMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) password_hash = db.Column(db.String(128)) # 不保存原始密码 ... def get_jwt(self, expires_in=600): now = datetime.utcnow() payload = { 'user_id': self.id, 'name': self.name if self.name else self.username, 'exp': now + timedelta(seconds=expires_in), 'iat': now } return jwt.encode( payload, current_app.config['SECRET_KEY'], algorithm='HS256').decode('utf-8') @staticmethod def verify_jwt(token): try: payload = jwt.decode( token, current_app.config['SECRET_KEY'], algorithms=['HS256']) except (jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidSignatureError) as e: # Token过期,或被人修改,那么签名验证也会失败 return None return User.query.get(payload.get('user_id'))
在配置文件 back-end/config.py
中添加配置项: SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
修改 back-end/app/api/tokens.py
:
from flask import jsonify, g from app import db from app.api import bp from app.api.auth import basic_auth @bp.route('/tokens', methods=['POST']) @basic_auth.login_required def get_token(): token = g.current_user.get_jwt() db.session.commit() return jsonify({'token': token})
注意:
JWT
没办法回收(不需要DELETE /tokens
),只能等它过期,所以有效时间别设置太长
1.2 Flask-Migrate 迁移
修改 User 数据模型后要进行数据库迁移:
(venv) D:\python-code\flask-vuejs-madblog\back-end>flask db migrate -m "users jwt" (venv) D:\python-code\flask-vuejs-madblog\back-end>flask db upgrade ... sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "DROP": syntax error [SQL: 'ALTER TABLE user DROP COLUMN token_expiration'] (Background on this error at: http://sqlalche.me/e/e3q8)
注意: 报错的原因是
SQLite
默认不支持删除列
可以使用 batch_alter_table
上下文管理器来解决此问题,参考: https://stackoverflow.com/questions/30394222/why-flask-migrate-cannot-upgrade-when-drop-column ,需要修改 back-end/migrations/env.py
:
def run_migrations_online(): ... context.configure(connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, render_as_batch=True, # 增加这个配置项 **current_app.extensions['migrate'].configure_args)
删除刚才新创建的迁移脚本,比如我的是 back-end/migrations/versions/8ba2e85acf5b_users_jwt.py
。然后再次创建迁移脚本:
查看新迁移脚本,会发现使用了 batch_alter_table
上下文管理器:
def upgrade(): # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table('user', schema=None) as batch_op: batch_op.drop_column('token') batch_op.drop_column('token_expiration') # ### end Alembic commands ###
最后,应用迁移脚本:
1.3 JSON.parse
前端用户在登录页面输入用户名和密码后,axios
调用 POST /tokens
。后端首先要进行 Basic Auth
认证, 如果用户名和密码正确,则进入视图函数 get_token
中执行并返回 JWT
给前端
用户 id
存放在 JWT
的第二部分中(三个部分以点号 .
分隔),所以前端可以使用如下方法解析 JSON 数据:
修改 front-end/src/store.js
:
export default { debug: true, state: { is_authenticated: window.localStorage.getItem('madblog-token') ? true : false, // 用户登录后,就算刷新页面也能再次计算出 user_id user_id: window.localStorage.getItem('madblog-token') ? JSON.parse(atob(window.localStorage.getItem('madblog-token').split('.')[1])).user_id : 0 }, loginAction () { if (this.debug) { console.log('loginAction triggered') } this.state.is_authenticated = true this.state.user_id = JSON.parse(atob(window.localStorage.getItem('madblog-token').split('.')[1])).user_id }, logoutAction () { if (this.debug) console.log('logoutAction triggered') window.localStorage.removeItem('madblog-token') this.
7 条评论
评论者的用户名
评论时间LongchuanYu
2019-12-05T03:09:54Z问题描述:
后台如果不用JWT,想要实现 “调用 GET /api/users/id 来获取自己的用户信息”。
是否可以在token.py的get_token()方法中,返回一个id给前端呢?
Madman LongchuanYu Author
2019-12-05T04:54:19Z你可以在后端返回任何数据给前端! 但是,我不知道你的目的是啥,认证可以用 jwt,或者像第三章那样用数据库存 token。 你返回UID 给前端干什么用? 假设管理员的 UID 为 1,难道前端凭这个 UID 就能像管理员一样查看与操作特权内容吗?
LongchuanYu Madman
2019-12-05T07:01:02Z因为上一章从数据库存token变成了jwt,文章提到改变的目的是为了获得自己的用户id。
我在想如果用上一章的方法有没有办法实现这个功能,于是就提出了这个问题。
但是没考虑到你说的安全问题。感谢博主的解答!目前正在学习中..。
FutureSenzhong
2020-02-06T08:32:15Z直接用的附件里面的最新完整后端代码,现在前端这个章节写完了登录后不跳转,请求支援
Madman FutureSenzhong Author
2020-02-06T12:16:26Z加我微信,发出错的前后端日志截图看看先
delin
2022-02-24T08:44:04ZFile "D:\var\webapp\flask-vuejs-madblog\back-end\app\models.py", line 96, in get_jwt return jwt.encode( AttributeError: 'str' object has no attribute 'decode'
delin
2022-02-24T09:17:18Z不知道那里出问题了,这里弄了很久,用你的源码也不行