REST要求无状态,可以理解为没有session,而且session的存储遇到分布式集群的情况就比较难搞,所以对于用户验证目前网上大多数做法是token方式,第一次登录的时候,先提交用户名密码,服务器收集到以后,先验证一下,如果验证通过了,这时候服务器端基于用户名、密码、当前时间戳等内容,用md5或者des或者aes等加密方式,生成一个token值,然后把token值存放到redis里面,记录它对应哪个用户,然后把这个token值发给客户端。客户端收到token值以后,下次访问服务器端任何接口的时候,直接携带这个token,服务器端就知道它是谁了,该给它什么数据。哪以何种方式给服务器端呢?通常的做法就是把token值放在header里面。当然这个不是绝对,你也可以放在body里面,这个都是仁者见仁智者见智的事。好了,我们就先放在header头里面。
引自《使用Flask设计带认证token的RESTful API接口[翻译]》
因为需要每次请求都要发送用户名和密码,客户端需要把验证信息存储起来进行发送,这样十分不方便,就算在HTTPS下的传输,也是有风险存在的。
比前面的密码验证方法更好的是使用Token认证请求。
原理是第一次客户端与服务器交换过认证信息后得到一个认证token,后面的请求就使用这个token进行请求。Token通常会给一个过期的时间,当超过这个时间后,就会变成无效,需要产生一个新的token。这样就算token泄漏了,危害也只是在有效的时间内。
好多种办法去实现token。一种简单的做法就是产生一个固定长度的随机序列字符与用户名和密码一同存储在数据库当中,有可能带上一个过期时间。这样token就变成了一串普通的字符,可以十分容易地和其它字符串验证对比,并且可以检查时间是否过期。
更复杂的实现办法是不需要服务器端进行存储token,而是使用数字签名信息作为token。这样做的好处是经过用户数字签名生成的token是可以防篡改的。
Flask使用与数字签名有些相似的办法去实现加密的cookies的,这里我们使用itsdangerous的库去实现。
生成token和验证token的方法可以附加到User model上实现:
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
class User(db.Model):
# ...
def generate_auth_token(self, expiration = 600):
s = Serializer(app.config['SECRET_KEY'], expires_in = expiration)
return s.dumps({ 'id': self.id })
@staticmethod
def verify_auth_token(token):
s = Serializer(app.config['SECRET_KEY'])
try:
data = s.loads(token)
except SignatureExpired:
return None # valid token, but expired
except BadSignature:
return None # invalid token
user = User.query.get(data['id'])
return user
在generate_auth_token()函数中,token其实就是一个加密过的字典,里面包含了用户的id和默认为10分钟(600秒)的过期时间。
verify_auth_token()的实现是一个静态方法,因为token只是一次解码检索里面的用户id。获取用户id后就可以在数据库中取得用户资料了。
试试使用一个新的接入点,让客户端请求一个token:
@app.route('/api/token')
@auth.login_required
def get_auth_token():
token = g.user.generate_auth_token()
return jsonify({ 'token': token.decode('ascii') })
注意,这个接入点是被Flask-HTTPAuth扩展的auth.login_required装饰器保护的,请求需要提供用户名和密码。
上面返回的是一个token字符串,下面的请求将会包含这个token。
HTTP Basic Authentication协议没有具体要求必需使用用户名和密码进行验证,HTTP头可以使用两个字段去传输认证信息,对于token认证,只需要把token当成用户名发送即可,密码字段可以乎略。
综上所说,一些认证还是要使用用户名和密码认证,另外一部份直接使用获取的token认证。verify_password回调函数则需要包括两种验证的方式:
@auth.verify_password
def verify_password(username_or_token, password):
# first try to authenticate by token
user = User.verify_auth_token(username_or_token)
if not user:
# try to authenticate with username/password
user = User.query.filter_by(username = username_or_token).first()
if not user or not user.verify_password(password):
return False
g.user = user
return True
修改原来的verify_password回调函数,添加两种验证。开始用用户名字段当作token,如果不是token来的,就采用用户名和密码验证。
使用curl测试请求获取一个认证token:
$ curl -u ok:python -i -X GET http://127.0.0.1:5000/api/token
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 139
Server: Werkzeug/0.9.4 Python/2.7.3
Date: Thu, 28 Nov 2013 20:04:15 GMT
{
"token": "eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4NTY2OTY1NSwiaWF0IjoxMzg1NjY5MDU1fQ.eyJpZCI6MX0.XbOEFJkhjHJ5uRINh2JA1BPzXjSohKYDRT472wGOvjc"
}
成功,再试试使用token一访问受保护的API:
$ curl -u eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4NTY2OTY1NSwiaWF0IjoxMzg1NjY5MDU1fQ.eyJpZCI6MX0.XbOEFJkhjHJ5uRINh2JA1BPzXjSohKYDRT472wGOvjc:unused -i -X GET http://127.0.0.1:5000/api/resource
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 30
Server: Werkzeug/0.9.4 Python/2.7.3
Date: Thu, 28 Nov 2013 20:05:08 GMT
{
"data": "Hello, ok!"
}
介绍见下一章节链接,我这边列出我的项目代码做笔记
首先在数据库用户表中增加一个token字段(uuid随机数),这是用来控制token回收考虑,避免token放出去后只受过期时间控制,系统无法掌控它
然后在利用JWT规则生成一个对外的token,这个token过期时间相对较短,只受过期时间影响,只要拿到这个对外的token就可以访问系统(符合JWT原则)
验证token时按照JWT规则验证即可(这里我还对数据库中的token进行比对,这是不符合JWT规则的,后续再修改),ps:因为我这系统本身另外用局域网单点登录,所以用了cas,本身不存用户密码
# app/commmon/auty.py
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpired, BadSignature
# token生成函数
def generate_token(user, expiration = 36000):
s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
return s.dumps({'name': user.name, 'token': user.token}).decode('ascii')
# token验证
def verify_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except SignatureExpired:
raise MyHttpAuthFailed('token expired')
# return {'message': 'token expired'}, return_code.Unauthorized#token_expired() # valid token, but expired
except BadSignature:
raise MyHttpAuthFailed('token invalid')
# return {'message':'token invalid'}, return_code.Unauthorized #invalid_token() # invalid token
except:
raise MyHttpAuthFailed('wrong token with unknown reason')
user = User.query.filter_by(name=data['name']).first()
if user is None or user.token != data['token']:
raise MyHttpAuthFailed('token invalid')
# return {'message': 'token invalid'}, return_code.Unauthorized #invalid_token()
return user, return_code.Successful
def login():
return flask.redirect(flask.url_for('cas.login', _external=True))
def logout():
return flask.redirect(flask.url_for('cas.logout', _external=True))
# 登录验证(会调用token验证函数)
def login_required(function):
@wraps(function)
def wrap(*args, **kwargs):
if 'token' not in flask.request.headers and 'CAS_USERNAME' not in flask.session:
flask.session['CAS_AFTER_LOGIN_SESSION_URL'] = flask.request.path
return login()
if 'token' in flask.request.headers:
# token验证
resp, status = verify_token(flask.request.headers.get('token'))
if status != return_code.Successful:
return resp,status
user = resp
else:
# CAS验证
username = flask.session["CAS_USERNAME"].lower()
user = User.query.filter_by(
name=username).first()
if not user: # 登录成功, 但数据库不存在,则新建默认权限用户,并分配给一个TOKEND
user = User(name=username)
db.session.add(user)
db.session.commit()
g.current_user = user
return function(*args, **kwargs)
return wrap
然后在登录后将生成的token传递给前端
# app/api/v1/auth/auth.py
@login_required
@sso_ns.route('/afterlogin')
class afterLoginAction(Resource):
"""
Do something after SSO login
"""
def get(self):
app = current_app._get_current_object()
r = make_response('Accepted', 302)
r.headers['Location'] = '/' #'http://127.0.0.1'
r.set_cookie("username", cas.username)
user = User.query.filter_by(name=cas.username.lower()).first()
if user:
r.set_cookie("userrole", user.role.name)
else:
# 不存在这个用户,默认新建作为普通用户
user = User(name=cas.username)
db.session.add(user)
db.session.commit()
# r.set_cookie("token", user.generate_token())
r.set_cookie("token", generate_token(user))
return r
相比上一个方法,加一个刷新token
一个为access token,用于用户后续的各个请求中携带的认证信息
另一个是refresh token,为access token过期后,用于申请一个新的access token。
由此可以给两类不同token设置不同的有效期,例如给access token仅1小时的有效时间,而refresh token则可以是一个月。api的登出通过access token的过期来实现(前端则可直接抛弃此token实现登出),在refresh token的存续期内,访问api时可执refresh token申请新的access token(前端可存此refresh token,access token过其实进行更新,达到自动延期的效果)。
refresh token不可再延期,过期需重新使用用户名密码登录。
access token 短期证书,用于最终鉴权
refresh token 较长期的证书,用于产生短期证书,不可直接用于服务请求
用户名密码 几乎永久的证书,用于产生长期证书和短期证书,不可直接用于服务请求
我们做了一个JWT的认证模块:
(access token在以下代码中为’token’,refresh token在代码中为’rftoken’)
首次认证
client -----用户名密码-----------> server
client <------token、rftoken----- server
access token存续期内的请求
client ------请求(携带token)----> server
client <-----结果----------------- server
access token超时
client ------请求(携带token)----> server
client <-----msg:token expired— server
重新申请access token
client -请求新token(携带rftoken)-> server
client <-----新token-------------- server
rftoken token超时
client -请求新token(携带rftoken)-> server
client <----msg:rftoken expired— server
如果设计一个针对此认证的前端,需要:
存储access token、refresh token
访问时携带access token,自动检查access token超时,超时则使用refresh token更新access token;状态延期用户无感知
用户登出直接抛弃access token与refresh token
使用python实现后台系统的JWT认证(转)
链接:https://www.cnblogs.com/wayneiscoming/p/7513487.html
转自
作者:茶客furu声
链接:http://www.jianshu.com/p/537b356d34c9
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。