作者介绍
张龙(zero),一线运维老鸟。致力于LINUX/PYTHON/开源技术研究。
常见认证方法
首先要明白,认证和授权是不同的。认证是判定用户的合法性,授权是判定用户的权限级别是否可执行后续操作。这里所讲的仅含认证。
basic认证
这是http协议中所带带基本认证,是一种简单为上的认证方式。原理是在每个请求的header中添加用户名和密码的字符串(格式为“username:password”,用base64编码)。这种方式相当于将“用户名:密码”绑定为一个开放式证书,这会有几个问题:
1.每次请求都需要用户名密码,如果此连接未使用SSL/TLS,或加密被破解,用户名密码基本就暴露了
2.无法注销用户的登录状态
3.证书不会过期,除非修改密码。
cookie
将认证的结果存在客户端的cookie中,通过检查cookie中的身份信息来作为认证结果。这种方式的特点是便捷,且只需要一次认证,多次可用;也可以注销登录状态和设置过期时间;甚至也有办法(比如设置httpOnly)来避免XSS攻击。但它的缺点十分明显,使用cookie那便是有状态的服务了。
JWT
JWT协议似乎已经应用十分广泛,JSON Web Token——一种基于token的json格式web认证方法。基本的原理是,第一次认证通过用户名密码,服务端签发一个json格式的token。后续客户端的请求都携带这个token,服务端仅需要解析这个token,来判别客户端的身份和合法性。而JWT协议仅仅规定了这个协议的格式(RFC7519),它的序列生成方法在JWS协议中描述(https://tools.ietf.org/html/rfc7515),分为三个部分:
header头部
header头部主要用于声明类型,这里是jwt,声明加密的算法 通常直接使用 HMAC SHA256。一种常见的头部是这样的:
{
'typ': 'JWT',
'alg': 'HS256'
}
1
2
3
4
{
'typ':'JWT',
'alg':'HS256'
}
payload负荷
payload是放置实际有效使用信息的地方。JWT定义了几种内容,包括:标准中注册的声明,如签发者,接收者,有效时间(exp),时间戳(iat,issued at)等;为官方建议但非必须公共声明私有声明 一个常见的payload是这样的:
{'user_id': 123456,
'user_role': admin,
'iat': 1467255177
}
1
2
3
4
{'user_id':123456,
'user_role':admin,
'iat':1467255177
}
事实上,payload中的内容是自由的,按照自己开发的需要加入。
有个小问题。使用itsdangerous包的TimedJSONWebSignatureSerializer进行token序列生成的结果,exp是在头部里的。这里似乎违背了jwt的协议规则。
实现JWT
如何生成token
这里使用python模块itsdangerous,这个模块能做很多编码工作,其中一个是实现JWS的token序列。 genTokenSeq这个函数用于生成token。其中使用的是TimedJSONWebSignatureSerializer进行序列的生成,这里secretkey密钥、salt盐值从配置文件中读取,当然也可以直接写死在这里。expiresin是超时时间间隔,这个间隔以秒记,可以直接在这里设置,选择将其设为方法的形参。
# serializer for JWT
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
def genTokenSeq(self, expires):
s = Serializer(
secret_key=app.config['SECRET_KEY'],
salt=app.config['AUTH_SALT'],
expires_in=expires)
timestamp = time.time()
return s.dumps(
{'user_id': self.user_id,
'user_role': self.role_id,
'iat': timestamp})
1
2
3
4
5
6
7
8
9
10
11
12
# serializer for JWT
fromitsdangerousimportTimedJSONWebSignatureSerializerasSerializer
defgenTokenSeq(self,expires):
s=Serializer(
secret_key=app.config['SECRET_KEY'],
salt=app.config['AUTH_SALT'],
expires_in=expires)
timestamp=time.time()
returns.dumps(
{'user_id':self.user_id,
'user_role':self.role_id,
'iat':timestamp})
使用Serializer可以帮我们处理好header、signature的问题。我们只需要用s.dumps将payload的内容写进来。这里准备在每个token中写入三个值:用户id、用户角色id和当前时间(‘iat’是JWT标准注册声明中的一项)。
解析token
解析需要使用到同样的serializer,配置一样的secret key和salt,使用loads方法来解析token。itsdangerous提供了各种异常处理类,用起来也很方便。
如果是SignatureExpired,则可以直接返回过期;
如果是BadSignature,则代表了所有其他签名错误的情况,于是又分为:
能读取到payload:那么这个消息是一个内容被篡改、消息体加密过程正确的消息secret key和salt很可能泄露了;
不能读取到payload: 消息体直接被篡改,secret key和salt应该仍然安全。
以上内容写成一个函数,用于验证用户token。如果实现在python flask,可以考虑将此函数改为一个decorator修饰漆,将修饰器@到所有需要验证token的方法前面,则代码可以更加优雅。
# serializer for JWT
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
# exceptions for JWT
from itsdangerous import SignatureExpired, BadSignature, BadData
# Class xxx
def tokenAuth(token):
s = Serializer(
secret_key=api.app.config['SECRET_KEY'],
salt=api.app.config['AUTH_SALT'])
try:
data = s.loads(token)
except SignatureExpired:
msg = 'token expired'
app.logger.warning(msg)
return [None, None, msg]
except BadSignature, e:
encoded_payload = e.payload
if encoded_payload is not None:
try:
s.load_payload(encoded_payload)
except BadData:
msg = 'token tampered'
app.logger.warning(msg)
return [None, None, msg]
msg = 'badSignature of token'
app.logger.warning(msg)
return [None, None, msg]
except:
msg = 'wrong token with unknown reason'
app.logger.warning(msg)
return [None, None, msg]
if ('user_id' not in data) or ('user_role' not in data):
msg = 'illegal payload inside'
app.logger.warning(msg)
return [None, None, msg]
msg = 'user(' + data['user_id'] + ') logged in by token.'
userId = data['user_id']
roleId = data['user_role']
return [userId, roleId, msg]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# serializer for JWT
fromitsdangerousimportTimedJSONWebSignatureSerializerasSerializer
# exceptions for JWT
fromitsdangerousimportSignatureExpired,BadSignature,BadData
# Class xxx
deftokenAuth(token):
s=Serializer(
secret_key=api.app.config['SECRET_KEY'],
salt=api.app.config['AUTH_SALT'])
try:
data=s.loads(token)
exceptSignatureExpired:
msg='token expired'
app.logger.warning(msg)
return[None,None,msg]
exceptBadSignature,e:
encoded_payload=e.payload
ifencoded_payloadisnotNone:
try:
s.load_payload(encoded_payload)
exceptBadData:
msg='token tampered'
app.logger.warning(msg)
return[None,None,msg]
msg='badSignature of token'
app.logger.warning(msg)
return[None,None,msg]
except:
msg='wrong token with unknown reason'
app.logger.warning(msg)
return[None,None,msg]
if('user_id'notindata)or('user_role'notindata):
msg='illegal payload inside'
app.logger.warning(msg)
return[None,None,msg]
msg='user('+data['user_id']+') logged in by token.'
userId=data['user_id']
roleId=data['user_role']
return[userId,roleId,msg]
原文来自微信公众号:DevOps视角
本文链接:http://www.yunweipai.com/13797.html