作者按: 几天前我收到一封邮件,有读者说看了我的前后端分离实践的文章获益很多。然而我却丧尽天良的断更了?不行不行,我不是这样的人,所以一年后,我再补上这个系列最后一篇文章吧。
CSRF防护
如果你们是看了Miguel的狗书,或是李辉大大的狼书,一定知道我们在提交表单时,常常会附带上一个隐藏的csrf值,用来防止CSRF攻击。关于CSRF是什么这里就不过多介绍了,大家可以参阅维基百科。那么我们来到前后端分离的世界,CSRF应该如何做呢?因为是前后端分离,所以服务端产生的CSRF值并不能实时更新到页面上,页面的更新全都要依赖客户端去主动请求。那我是不是要每次渲染表单的时候,就去服务器取一次CSRF token呢?这未免太麻烦,我们完全可以减少请求的次数,请求一次,然后在客户端(浏览器)上存起来,要用的时候带上即可。
在Flask中引入CSRF保护主要是用Flask-WTF这个扩展,但既然我们不用WTF去渲染表单了,那么表单的CSRF保护也用不上了,所幸,这个扩展还提供了一个全局CSRF保护方法,就是所有view都可以通过一个模板变量去获取CSRF token的值,并不仅限于表单。开启方法也很简单:
Python
代码语言:javascript复制from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
# 或者使用工厂函数模式:
csrf = CSRFProtect()
def create_app():
app = Flask(__name__)
...
csrf.init_app(app)
return app
这样在模板中,可以通过{{ csrf_token() }}
获得CSRF token的值。推荐放在返回的前端页面index.html
的meta标签中,以供ajax方法获取
Html
代码语言:javascript复制...
<header>
<meta name="csrf-token" content="{{ csrf_token() }}">
...
然后在ajax请求中,取出这个值然后带上即可,这里展示一下如何用axios
实现:
Javascript
代码语言:javascript复制const api = axios.create({
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')}
})
这也是我这个todo项目采用的方法,但这种方法有一个很大的限制:前端页面必须至少由Flask应用渲染一次,这只能叫做半个前后端分离。实际开发中,前端和后端可能完全是分离部署,通过nginx等其他web服务器返回的。这样一来,{{ csrf_token() }}
就完全没机会透给前端。不要紧,我们还可以用Cookies嘛。当然,这需要自己定制一下Flask-WTF
这个扩展,可以查看这个代码示例。在Django中,默认采用的就是这种方式。
后端鉴权
好了,我们又用到了Cookie,如果有人对上一篇还有印象的话(并没有),用户的登录态也是放在cookie里面的,这种方案对于一般的普通应用就足够了,我一直提倡如果某种方法够用,就不用急着使用更高级的方法。但当某些客户端不支持cookie的时候(比如手机app),我们就需要新的方法了。
当然,这个解决方案现在也很成熟了,就是JWT(JSON Web Token)。大概流程是,第一次打开页面时,请求后端,如果没登录,则返回401让前端跳转登录,如果是登录状态,则返还一个Token,这个token自带某些用户信息,和过期时间。前端收到这个token则自己保存起来,保存方式可以是cookie,也可以是localstorage,然后后续的请求均带上这个token,前后端之间仅仅依靠这个token鉴定身份,无需来回传送cookie或会话信息。
JWT的好处是服务端无需保存这个token值,token本身就带有是否有效的信息,以及登录态的关键信息(比如user id),而token是通过服务端密钥加密的,所以难以被破解。Flask内置了一个itsdangerous
的库来生成这种token,先总结一下,Flask要做的事有:
- 每次请求都校验这个token值,若不通过则返回401
- login端点生成token值
- logout端点清除token值
Python
代码语言:javascript复制@app.before_request
def validate_request():
token = request.headers.get('X-Token')
if not token:
abort(401)
user = User.verify_token(token)
if not user:
abort(401)
g.current_user = user
@api.route('/user/login', methods=['POST'])
def login():
data = request.get_json()
if not verify_auth(data.get('username'), data.get('password')):
return jsonify(
{'code': 60204, 'message': 'Account and password are incorrect.'}
)
return jsonify({'code': 20000, 'data': {'token': g.user.generate_token().decode()}})
Python
代码语言:javascript复制from itsdangerous import (
TimedJSONWebSignatureSerializer as Serializer,
BadSignature,
SignatureExpired,
)
class User(db.Model):
...
@classmethod
def verify_auth_token(cls, token):
s = Serializer(current_app.config["SECRET_KEY"])
try:
data = s.loads(token)
except (BadSignature, SignatureExpired):
return None
user = cls.query.get(data["id"])
return user
def generate_token(self, expiration=24 * 60 * 60):
s = Serializer(current_app.config["SECRET_KEY"], expires_in=expiration)
return s.dumps({"id": self.id})
而前端请求ajax时,只需要把这个事先保存好的token值取出来加到请求头部X-Token
就可以了。
总结
好了,我想这三篇文章已经覆盖了前后端分离与传统MVC架构的主要区别和开发技巧,当然还有更多的点我没法覆盖到,欢迎到评论区或邮件骚扰我。