最近一直在负责开发公司的开放平台相关工作,对接淘宝,阿里巴巴等开放平台,同时也负责开发系统的开放平台,在此稍作总结。本文只稍微分析聊一下授权码模式,并且不尝试解释OAuth2.0参数为什么不是驼峰的……
参考资料
RFC6794
理解OAuth 2.0
使用场景
用户登录云管店
应用,此时没有办法直接登录阿里巴巴
应用查看数据,或者阿里巴巴
数据还未经过处理,不是用户的目标数据。
用户登录云管店
(假设该应用对接了阿里巴巴应用的接口)应用,查看自己门店当前的库存数量,同时为了更直观的了解到当前阿里巴巴
上挂的店铺的库存,云管店
要去访问阿里巴巴
接口拉取到该用户在阿里巴巴
的店铺的仓库数量,统计成报表。
如果不适用OAuth2.0
,云管店
应该如何读取到阿里巴巴
上的库存数量?
用户提供
阿里巴巴
账号密码给云管店
,云管店
通过账号密码即可读取到库存信息。那么这么做有带来什么隐患?
-
阿里巴巴
账号密码泄露给云管店
,云管店
可以任意获取用户在阿里巴巴
上的数据 -
云管店
数据库如果泄露,也把阿里巴巴
的账号密码等数据泄露出去 - 为了防止
云管店
任意读取数据,只能通过修改账号密码 - ...
基于数据开放,且为了保护用户数据安全等诸多问题,OAuth2.0应运而生,并成为当前最主流的解决方案。
OAuth2.0 解决方案
OAuth2.0
在客户端
与服务提供商
之间,设置了一个授权访问的屏障。客户端
无法直接拿到服务提供商
的登录账号密码,也就无法直接登录服务提供商
,只能请求授权服务提供商
。
此时会要求用户登录资源提供商
(该登录服务由服务提供商
提供,不会存在账号密码泄露等问题)。登录后,授权服务提供商
提示用户确认授权后提供给客户端
一个token
令牌。服务提供商
根据令牌的时效和授权范围,向客户端
开放数据。
OAuth2.0客户端授权模式
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。(本文只提到授权码模式,其他相关客户端授权模式请参考上文的参考资料进行了解)
流程解析
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
A步骤中,客户端申请认证的URI,包含以下参数:
- response_type:表示授权类型,必选项,此处的值固定为"code"
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的权限范围,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
C步骤中,服务器回应客户端的URI,包含以下参数:
- code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
- state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
- grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
- code:表示上一步获得的授权码,必选项。
- redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
- client_id:表示客户端ID,必选项。
E步骤中,认证服务器发送的HTTP回复,包含以下参数:
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
- scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
基于规范,动手实现一个简易版的授权码模式
对应于A步骤,客户端发起授权请求(该请求可以要求登录,用户访问该请求需要登录)。授权参数需要参照OAuth2.0
规范,最好是相应的参数名称都按照规范来。
@RequestMapping(value = "/authorize")
public String authorize(ModelMap modelMap, AuthorizeDTO authorizeDTO) {
// 如果是授权码模式
if(GrantTypeEnum.AUTHORIZATION_CODE.getValue().equals(authorizeDTO.getResponse_type())) {
// 检验客户信息
if(!StoreFactory.getClientStore().isContainsClientId(authorizeDTO.getClient_id())) {
ModelMapUtil.setMessage(modelMap, ResultMessage.ERROR_CLIENT_ID);
return returnErrorPage();
}
// 检验重定向地址
if(!StoreFactory.getClientStore().isContainsRedirectUri(authorizeDTO.getClient_id(), authorizeDTO.getRedirect_uri())) {
ModelMapUtil.setMessage(modelMap, ResultMessage.ERROR_REDIRECT_URI);
return returnErrorPage();
}
modelMap.put("client_id", authorizeDTO.getClient_id());
modelMap.put("redirect_uri", authorizeDTO.getRedirect_uri());
modelMap.put("state", authorizeDTO.getState());
}
return "/auth";
}
对应步骤C,确认授权后可以获取到相应的code与state等参数,附着在回调地址中,且该回调地址必须与申请资质时填写的回调的地址(申请资质需要客户端应用向服务提供商申请,由服务提供商颁发相应的key与secret)
@RequestMapping(value = "/confirm")
public String accessConfirm(ModelMap modelMap, AuthorizeDTO authorizeDTO) {
// 检验客户信息
if(!StoreFactory.getClientStore().isContainsClientId(authorizeDTO.getClient_id())) {
ModelMapUtil.setMessage(modelMap, ResultMessage.ERROR_CLIENT_ID);
return returnErrorPage();
}
// 检验重定向地址
if(!StoreFactory.getClientStore().isContainsRedirectUri(authorizeDTO.getClient_id(), authorizeDTO.getRedirect_uri())) {
ModelMapUtil.setMessage(modelMap, ResultMessage.ERROR_REDIRECT_URI);
return returnErrorPage();
}
// 根据填写的回调地址回调回去
return "redirect:" + authorizeDTO.getRedirect_uri()+"?code="+StoreFactory.getCodeStore().createUUIDCode(authorizeDTO.getClient_id())
+"&state="+authorizeDTO.getState();
}
对应步骤E,使用获取到的code去换取token,或者使用旧的refresh_token去获取新的token
@RequestMapping(value = "/token")
@ResponseBody
public ResultObject accessToken(ModelMap modelMap, AuthorizeTokenDTO authorizeTokenDTO) {
// 检验客户信息
if(!StoreFactory.getClientStore().isConatinsClient(authorizeTokenDTO.getClient_id(), authorizeTokenDTO.getClient_secret())) {
return ResultMessage.ERROR_CLIENT_ID.getResultObject();
}
// 检验重定向地址
if(!StoreFactory.getClientStore().isContainsRedirectUri(authorizeTokenDTO.getClient_id(), authorizeTokenDTO.getRedirect_uri())) {
return ResultMessage.ERROR_REDIRECT_URI.getResultObject();
}
// 检验code
if(!StoreFactory.getCodeStore().isRightCode(authorizeTokenDTO.getCode(), authorizeTokenDTO.getClient_id())) {
return ResultMessage.ERROR_CODE.getResultObject();
}
// 生成token
if(GrantTypeEnum.AUTHORIZATION_CODE.equals(authorizeTokenDTO.getGrant_type())) {
// 也可以根据redirect_uri 回调回去
// 也可以将返回值包装成Josn返回
//
return ResultMessage.SUCCESS.getResultObject(StoreFactory.getTokenStore().createUUIDToken(authorizeTokenDTO.getClient_id()));
}
// 刷新token
if(GrantTypeEnum.REFRESH_TOKEN.equals(authorizeTokenDTO.getGrant_type())) {
// 拿到refreshToken 并检验刷新
// 这里没有做实现,但是原理一致
return ResultMessage.SUCCESS.getResultObject(StoreFactory.getTokenStore().createUUIDToken(authorizeTokenDTO.getClient_id()));
}
return ResultMessage.ERROR_GRANT_TYPE.getResultObject();
}
如此简单便可以实现一个最简易的授权码模式的服务。麻雀虽小,却也五脏俱全,不能直接用于真实生产环境,但是对于理解OAuth2.0的授权过程却也足以。
代码地址:https://gitee.com/linweifeng/OAuth/tree/master
分布式环境
如果是单机应用,我们的授权服务,资源服务(开放的接口)都是可以统一放在一个应用上,那么实现自然是非常简单,通过拦截器/自定义注解实现AOP都可以做到非常完美,代码写起来也很6很顺手。
但是如果是分布式环境,比如现在最流行的微服务架构
就需要考虑的问题比较多,比如token
校验合法性。
授权服务
独立一个应用,功能简单,轻量.
资源服务
可能由于访问量较大,需要部署多台服务,通过负载均衡来保证服务稳定。
当客户端授权完成并成功拿到token
之后即可用它来访问资源服务,拉取数据。那么此时就需要校验token
的合法性,那么谁来校验token
才是最合适的呢?
资源服务提供者进行token校验
资源服务
提供token
合法性校验
- 资源服务需要校验
token
的合法性,相对复杂 - 受理了校验
token
合法性的业务,不能为其他应用提供服务,接口受制。
网管中心进行token校验
网管中心
是掌管一切请求的入口,在这一层做token
校验也是极为合理的。
- 就如同需要校验请求是否登录一样,在网管中心校验
token
- 接口不受理校验
token
合法性的业务,接口可以作为其他服务提供者。 - 实现相对复杂
授权服务进行token校验
授权服务
提供token
合法性校验,通过feign
将请求再转发到资源服务
- 把所有与授权相关的处理都统一在一个应用处理。
- 授权服务的压力甚至比
资源服务
压力更大,因为所有请求全都要经过授权服务
,所以授权服务
也需要多台部署。 - 接口不受理校验
token
合法性业务,接口可以作为其他服务提供者。
从架构上来说,更加推荐使用网管中心进行token校验,业务方接口方可复用。授权服务进行token检验亦有其优势,业务方接口亦可复用,但是服务压力大。
后记
OAuth2.0 目前已经被各大互联网公司所使用,足以证明它的优秀与不凡。