OAuth2.0是一个授权协议,用来解决第三方软件在访问受保护数据时,只有数据拥有者进行授权之后,第三方软件才能够进行访问。协议有四种授权类型,授权码、资源拥有者凭据许可、客户端凭据许可和隐式许可。
授权码许可
在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端(第三方软件)、授权服务和受保护资源。以下是一个例子:
用户小名通过小兔软件打印出他在京东上面的订单。对应关系就是,资源拥有者 -> 小明,第三方软件 -> 小兔软件,授权服务 -> 京东商家开放平台的授权服务,受保护资源 -> 小明店铺在京东上面的订单。
时序图
为什么需要授权码
从时序图中可以看到,用户授权之后,授权服务是先生成授权码,重定向到到第三方服务,通过post请求再生成访问令牌的。如果没有授权码,直接生成访问令牌,那重定向之后就会把访问令牌暴露在浏览器中,这非常不安全;如果不重定向,直接访问第三方软件后端,那用户授权之后,就与第三方软件“断连”了,无法交互了,体验不好。如下图:
授权过程
在第三方软件向授权服务申请访问令牌之前,需要向授权服务注册第三方软件的相关信息,注册之后得到app_id 、app_secret、重定向Url、权限范围 等信息,以方便后面授权时的各种身份校验。 随后才是产生授权码和访问令牌:
- 验证基本信息,包括对第三方软件小兔合法性和回调地址合法性的校验。
- 需要对传过来的 scope 参数,与注册时申请的权限范围做比对。如果请求过来的权限范围大于注册时的范围,就需要作出越权提示。
- 在授权服务中,需要将生成的授权码 code 值与 app_id、user 进行关系映射。也就是说,一个授权码 code,表示某一个用户给某一个第三方软件进行授权,比如小明给小兔软件进行的授权。同时,我们需要将 code 值和这种映射关系保存起来,以便在生成访问令牌 access_token 时使用。
String code = generateCode(appId,"USERTEST");//模拟登录用户为USERTEST
private String generateCode(String appId,String user) {
...
String code = strb.toString();
codeMap.put(code,appId+"|"+user+"|"+System.currentTimeMillis());
return code;
}
同时,授权服务还需要将生成的授权码 code 跟已经授权的权限范围 rscope 进行绑定并存储,以便后续颁发访问令牌时,我们能够通过 code 值取出授权范围并与访问令牌绑定。因为第三方软件最终是通过访问令牌来请求受保护资源的。
Map<String,String[]> codeScopeMap = new HashMap<String, String[]>();
codeScopeMap.put(code,rscope);//授权范围与授权码做绑定
- 重定向至第三方软件,生成授权码 code 值之后,授权服务需要将该 code 值告知第三方软件。
Map<String, String> params = new HashMap<String, String>();
params.put("code",code);
String toAppUrl = URLParamsUtil.appendParams(redirectUri,params);//构造第三方软件的回调地址,并重定向到该地址
response.sendRedirect(toAppUrl);//授权码流程的“第二次”重定向
- 颁发访问令牌 access_token,第三方软件后端带着授权码来请求访问令牌,在验证完成第三方软件身份以及code是否过期等之后,即可颁发令牌。需要将访问令牌 access_token 值存储起来,并将其与第三方软件的应用标识 app_id 和资源拥有者标识 user 进行关系映射。也就是说,一个访问令牌 access_token 表示某一个用户给某一个第三方软件进行授权。同时,授权服务还需要将授权范围跟访问令牌 access_token 做绑定。最后,还需要为该访问令牌设置一个过期时间 expires_in,比如 1 天。
- 在生成访问令牌的同时还会生成刷新令牌,这是为了在令牌过期的时候,可以通过刷新令牌重新获取访问令牌。
JWT机构化令牌
JWT 就是用一种结构化封装的方式来生成 token 的技术。结构化后的 token 可以被赋予非常丰富的含义,这也是它与原先毫无意义的、随机的字符串形式 token 的最大区别。
JWT 这种结构化体可以分为 HEADER(头部)、PAYLOAD(数据体)和 SIGNATURE(签名)三部分。经过签名之后的 JWT 的整体结构,是被句点符号分割的三段内容,结构为 header.payload.signature 。比如下面这个示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJVU0VSVEVTVCIsImV4cCI6MTU4NDEwNTc5MDcwMywiaWF0IjoxNTg0MTA1OTQ4MzcyfQ.
1HbleXbvJ_2SW8ry30cXOBGR9FW4oSWBd3PWaWKsEXE
HEADER 表示装载令牌类型和算法等信息,是 JWT 的头部。其中,typ 表示第二部分 PAYLOAD 是 JWT 类型,alg 表示使用 HS256 对称签名的算法。
PAYLOAD 表示是 JWT 的数据体,代表了一组数据。其中,sub(令牌的主体,一般设为资源拥有者的唯一标识)、exp(令牌的过期时间戳)、iat(令牌颁发的时间戳)是 JWT 规范性的声明,代表的是常规性操作。
SIGNATURE 表示对 JWT 信息的签名。那么,它有什么作用呢?我们可能认为,有了 HEADER 和 PAYLOAD 两部分内容后,就可以让令牌携带信息了,似乎就可以在网络中传输了,但是在网络中传输这样的信息体是不安全的,因为你在“裸奔”啊。所以,我们还需要对其进行加密签名处理,而 SIGNATURE 就是对信息的签名结果,当受保护资源接收到第三方软件的签名后需要验证令牌的签名是否合法。
令牌内检及如何使用JWT
保护资源来调用授权服务提供的检验令牌的服务,我们把这种校验令牌的方式称为令牌内检。授权服务“扔出”一个令牌,受保护资源服务“接住”这个令牌,然后自己开始解析令牌本身所包含的信息就可以了,而不需要再去查询数据库或者请求 RPC 服务。
其他许可类型
资源拥有者凭据
资源拥有者的凭据,就是用户的凭据,就是用户名和密码。当获取资源的软件是官方软件时,可以采用这种类型的许可换取token。
客户端凭据许可
如果没有明确的资源拥有者,比如获取京东 LOGO 的图片地址,这个 LOGO 信息不属于任何一个第三方用户,再比如其它类型的第三方软件来访问平台提供的省份信息,省份信息也不属于任何一个第三方用户。此时,在授权流程中,就不再需要资源拥有者这个角色了。这种场景下的授权,便是客户端凭据许可,第三方软件可以直接使用注册时的 app_id 和 app_secret 来换回访问令牌 token 的值。
隐式许可
第三方软件直接嵌入浏览器中,没有后端服务,这个时候通过app_id直接获取access_token,安全程度最低。
文本内容和图片部分来自极客时间课程《OAuth2.0实战课》。