最近想做个小程序,需要用到授权认证流程。以前项目都是用的 OAuth2 认证,但是 Sanic 使用 OAuth2 不太方便,就想试一下 JWT 的认证方式。
这一篇主要内容是 JWT 的认证原理,以及 python 使用 jwt 认识的实践。
在 HTTP 中,基本认证是一种用来允许 web 浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证的一种登录验证方式,通常用户名和明码会通过 HTTP 头传递。
- HTTP Basic Auth
在发送之前是以用户名追加一个冒号然后串接上口令,并将得出的结果字符串再用 Base64 算法编码。例如,提供的用户名是 Aladdin、口令是 open sesame,则拼接后的结果就是 Aladdin:open sesame,然后再将其用
,得到 QWxhZGRpbjpvcGVuIHNlc2FtZQ==。最终将 Base64 编码的字符串发送出去,由接收者解码得到一个由冒号分隔的用户名和口令的字符串。
- Base64编码
- 优点
基本认证的一个优点是基本上所有流行的网页浏览器都支持基本认证。
- 缺点
由于用户名和密码都是 Base64 编码的,而 Base64 编码是可逆的,所以用户名和密码可以认为是明文。所以只有在客户端和服务器主机之间的连接是安全可信的前提下才可以使用。
接下来我们看一个更加安全也适用范围更大的认证方式
。
- OAuth
OAuth 是一个关于授权(authorization)的开放网络标准。允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。现在的版本是 2.0 版。
严格来说,OAuth2 不是一个标准协议,而是一个安全的授权框架。它详细描述了系统中不同角色、用户、服务前端应用(比如 API),以及客户端(比如网站或移动 App)之间怎么实现相互认证。
OAuth 2.0 运行流程如图:
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
- 优点
快速开发
实施代码量小
维护工作减少
如果设计的 API 要被不同的 App 使用,并且每个 App 使用的方式也不一样,使用 OAuth2 是个不错的选择。
:
- 缺点
OAuth2 是一个安全框架,描述了在各种不同场景下,多个应用之间的授权问题。有海量的资料需要学习,要完全理解需要花费大量时间。
OAuth2 不是一个严格的标准协议,因此在实施过程中更容易出错。
了解了以上两种方式后,现在终于到了本篇的重点,JWT 认证。
, 根据官网的定义,是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519). 该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。
- Json web token (JWT)
JWT 是 Auth0 提出的通过对 JSON 进行加密签名来实现授权验证的方案,编码之后的 JWT 看起来是这样的一串字符:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
由
分为三段,通过解码可以得到:
- .
- // 包括类别(typ)、加密算法(alg);
- {
- "alg": "HS256",
- "typ": "JWT"
- }
jwt 的头部包含两部分信息:
然后将头部进行 base64 加密(该加密是可以对称解密的),构成了第一部分。
- eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷就是存放有效信息的地方。这些有效信息包含三个部分:
- 公共的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。
- 私有的声明 :
下面是一个例子:
- // 包括需要传递的用户信息;
- {
- "iss": "Online JWT Builder",
- "iat": 1416797419,
- "exp": 1448333419,
- "aud": "www.gusibi.com",
- "sub": "uid",
- "nickname": "goodspeed",
- "username": "goodspeed",
- "scopes": ["admin", "user"]
- }
其他还有:
将上面的 JSON 对象进行
可以得到下面的字符串。这个字符串我们将它称作 JWT 的 Payload(载荷)。
- base64编码
- eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0
:由于这里用的是可逆的 base64 编码,所以第二部分的数据实际上是明文的。我们应该避免在这里存放不能公开的隐私信息。
- 信息会暴露
- // 根据alg算法与私有秘钥进行加密得到的签名字串;
- // 这一段是最重要的敏感信息,只能在服务端解密;
- HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), SECREATE_KEY)
jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:
将上面的两个编码后的字符串都用句号. 连接在一起(头部在前),就形成了:
- eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
最后,我们将上面拼接完的字符串用 HS256 算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。如果我们用
作为密钥的话,那么就可以得到我们加密后的内容:
- secret
- pq5IDv - yaktw6XEa5GEv07SzS9ehe6AcVSdTj0Ini4o
将这三部分用. 连接成一个完整的字符串, 构成了最终的 jwt:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0.pq5IDv - yaktw6XEa5GEv07SzS9ehe6AcVSdTj0Ini4o
:签名实际上是对头部以及载荷内容进行签名。所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
- 签名的目的
这样就能保证 token 不会被篡改。
token 生成好之后,接下来就可以用 token 来和服务器进行通讯了。
下图是 client 使用 JWT 与 server 交互过程:
这里在第三步我们得到 JWT 之后,需要将 JWT 存放在 client,之后的每次需要认证的请求都要把 JWT 发送过来。(请求时可以放到 header 的 Authorization )
JWT 的主要优势在于使用无状态、可扩展的方式处理应用中的用户会话。服务端可以通过内嵌的声明信息,很容易地获取用户的会话信息,而不需要去访问用户或会话的数据库。在一个分布式的面向服务的框架中,这一点非常有用。
但是,如果系统中需要使用黑名单实现长期有效的 token 刷新机制,这种无状态的优势就不明显了。
- 优点
快速开发
不需要 cookie
JSON 在移动端的广泛应用
不依赖于社交登录
相对简单的概念理解
- 缺点
Token 有长度限制
Token 不能撤销
需要 token 有失效时间限制 (exp)
我基本是使用 python 作为服务端语言,我们可以使用 pyjwt:https://github.com/jpadilla/pyjwt/
使用比较方便,下边是我在应用中使用的例子:
- import jwt import time
- #使用sanic作为restful api框架def create_token(request) : grant_type = request.json.get('grant_type') username = request.json['username'] password = request.json['password']
- if grant_type == 'password': account = verify_password(username, password) elif grant_type == 'wxapp': account = verify_wxapp(username, password) if not account: return {}
- payload = {
- "iss": "gusibi.com",
- "iat": int(time.time()),
- "exp": int(time.time()) + 86400 * 7,
- "aud": "www.gusibi.com",
- "sub": account['_id'],
- "username": account['username'],
- "scopes": ['open']
- }
- token = jwt.encode(payload, 'secret', algorithm = 'HS256') return True,
- {
- 'access_token': token,
- 'account_id': account['_id']
- }
- def verify_bearer_token(token) : #如果在生成token的时候使用了aud参数,那么校验的时候也需要添加此参数payload = jwt.decode(token, 'secret', audience = 'www.gusibi.com', algorithms = ['HS256']) if payload: return True,
- token
- return False,
- token
这里,我们可以使用 jwt 直接生成 token,不用手动 base64 加密和拼接。
详细代码可以参考 gusibi/Metis: 一个测试类小程序(包含前后端代码) 。
这个项目中,api 使用 python sanic,文档使用 swagger-py-codegen 生成,提供 swagger ui。
现在可以使用 swagger ui 来测试 jwt。
这一篇主要介绍了 jwt 的原理、验证步骤,最后是使用
包演示 生成 token 以及校验 token 的方法。
- pyjwt
以上提到的包可以在
获取地址
- 公号回复关键字
最后,感谢女朋友支持。
欢迎关注 (April_Louisa) | 请我喝芬达 |
---|---|
来源: http://www.tuicool.com/articles/nyaU3iV