几种认证方法
basic auth
这个是 HTTP 协议中所带的基本认证功能。原理为在每个请求的 headers 中携带用户名和密码。
特点就是简单,但是却不是很安全。
cookie
将认证结果存储在浏览器的 cookie 中,后面通过检查 cookie 来校验认证信息。
特点是一次认证多次使用,但是却属于有状态的服务,对于分布式的服务端来说,管理 cookie 是个问题。
JWT
JWT(JSON Web Token) 是一种协议,它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。基本的原理是,第一次认证通过用户名密码,服务端签发一个 JSON 格式的 token。后续客户端的请求都携带这个 token,服务端仅需要解析这个 token,来判别客户端的身份和合法性。
Python 实现 JWT token
生成 token
使用 Python,可以很方便的生成一个 JWT 的 token
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer secret_key = 'hardtoguess' salt = 'hardtoguess' access_token_expires_in = 1800 refresh_token_expires_in = 86400 def genTokenSeq(user): u = User.query.filter_by(username=user).first() if u: userid = u.id else: return {'code': 422, 'message': '用户不存在'} access_token_gen = Serializer(secret_key=secret_key, salt=salt, expires_in=access_token_expires_in ) refresh_token_gen = Serializer(secret_key=secret_key, salt=salt, expires_in=refresh_token_expires_in ) timtstamp = time.time() access_token = access_token_gen.dumps({ "userid": userid, "iat": timtstamp }) refresh_token = refresh_token_gen.dumps({ "userid": userid, "iat": timtstamp }) data = { "access_token": str(access_token, 'utf-8'), "access_token_expire_in": access_token_expires_in , "refresh_token": str(refresh_token, 'utf-8'), "refresh_token_expire_in": refresh_token_expires_in , } return data
因为我们产生 token 是用来登陆的,所以我在 token 中增加了 userid,同时还增加了当前时间戳。这里产生了两个 token,分别是 access token 和 refresh token,access token 就是用户登陆成功后,发放给前端的,后面用户访问其他接口,都需要携带这个 token 来校验,refresh token 我们后面再解释。
解析token
解析 token 其实类似,只不过需要对不同的 token 错误做判断校验
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from itsdangerous import SignatureExpired, BadSignature, BadData def validateToken(token): s = Serializer(secret_key=secret_key, salt=salt) try: data = s.loads(token) except SignatureExpired: msg = 'toekn expired' return {'code': 401, 'error_code': 'auth_01', 'message': msg} except BadSignature as e: encoded_payload = e.payload if encoded_payload is not None: try: s.load_payload(encoded_payload) except BadData: msg = 'token tampered' return {'code': 401, 'error_code': 'auth_02', 'message': msg} msg = 'badSignature of token' return {'code': 401, 'error_code': 'auth_03', 'message': msg} except: msg = 'wrong token with unknown reason' return {'code': 401, 'error_code': 'auth_04', 'message': msg} if 'userid' not in data: msg = 'illegal payload inside' return {'code': 401, 'error_code': 'auth_05', 'message': msg} msg = 'user(' + str(data['userid']) + ') logged in by token.' userId = data['userid'] return {'code': 200, 'error_code': 'auth_00', 'message': msg, 'userid': userId}
方便起见,所有的 token 错误,都返回了401这个错误码,其实是应该再细分下的。
Vue 前端整合
再来看看前端需要做的事情,其实无非登陆成功后是把拿到的 token 保存起来,在调用其他接口时把 HTTP headers 中增加 token 信息就好了。
登陆接口
localStorage.setItem("accessToken", tokenInfo['access_token']); localStorage.setItem("refreshToken", tokenInfo['refresh_token']); localStorage.setItem("refreshTokenExpiryTime", tokenInfo['refresh_token_expire_in']); localStorage.setItem("accessTokenExpiryTime", tokenInfo['access_token_expire_in']);
把 token 信息等存储在 localStorage 中。
设置 header 方法
function setToken() { var JWT = 'jwt ' Axios.defaults.headers['Authorization'] = JWT + localStorage.getItem('accessToken'); }
在 header 中的 Authorization 中加入 jwt + token,后面所有的 API 请求,就都会带着这个 token 到后端了。
我们找一个前端用过的一个 javascript 函数,简单修改下
getData:function(){ //let data = {}; dealdata.getfile() .then(res => { console.log(res.data); if(res.data.code == 200) { if (res.data.data.length == 0){ this.files = [] }else{ this.files = res.data.data; } }else if(res.data.code == 401){ this.$message.error("登陆过期"); this.$router.push({path: '/login'}); }else{ console.log("API 调用错误"); } }) .catch(function(error){ console.log("网络错误"); }) }
代码很简单,就是判断下后端返回的 code 值,如果为 401,则设置当前 url 到 /login
修改后台接口
因为我们后期可能会有很多接口都需要用到函数 validateToken 来判断 token,为了方便优雅起见,使用装饰器就是最好的选择了
首先构造一个校验 token 的装饰器
def tokenRequired(f): @wraps(f) def decorated_function(*args, **kwargs): if 'Authorization' in request.headers: split_token = request.headers['Authorization'].split(' ') if len(split_token) == 2 and split_token[0] == 'jwt': token = request.headers['Authorization'].split(' ')[1] else: return {'code': 401, 'message': 'authorize failed'} else: return {'code': 401, 'message': 'authorize failed'} validator = validateToken(token) if validator['code'] != 200: return validator elif validator['code'] == 200: return f(*args, **kwargs) return f(*args, **kwargs) return decorated_function
在代码中,首先校验了 headers 的格式是否正确,如果正确,则开始判断函数 validateToken 的值,如果也通过了,才最终返回到被装饰的函数。
把装饰器装饰在 API 函数上
class FileListView(Resource): @token.tokenRequired def get(self): try: files = os.listdir(PYTEST_DIR) file_list = [] i = 0 for f in files: file_dict = {} if f[-4:] == 'xlsx' and not f.startswith('~'): file_dict["value"] = i file_dict["label"] = f file_list.append(file_dict) i += 1 return {'code': 200, 'data': file_list} except: raise
这个函数也是我们以前用过的,现在该函数在接收到没有携带正确 token 的请求时,是无法正确返回数据的了。
存在的问题
token 在生成之后,是靠 expire 使其过期失效的。签发之后的 token,是无法收回修改的,因此涉及 token 的有效期是个难题,主要体现在如下两个问题上:
- 用户登出
- token 过期续期
这里先给出实现方案,具体实现我们下次再细说
- 用户登出
前端可以直接丢弃当前的 access token,当然,如果再严谨些,后端最好有一个 redis 之类的缓存数据库,如果用户登出,则把对应的 token 加入到缓存中,如果再有请求携带该 token 时,则要先到缓存中查看 token 是否存在,如果存在,那么就要返回该 token 已经是非法的 token 了。 - token 过期续期
这个问题就可以用到 refresh token 了,当前端根据 access token expire 发现用户的 access token 快要过期时,则使用 refresh token 到后端获取新的 access token,只要保证 refresh token 的过期时间长于 access token 的就可以了。