OAuth2.0授权

最近一直在负责开发公司的开放平台相关工作,对接淘宝,阿里巴巴等开放平台,同时也负责开发系统的开放平台,在此稍作总结。本文只稍微分析聊一下授权码模式,并且不尝试解释OAuth2.0参数为什么不是驼峰的……

参考资料


RFC6794
理解OAuth 2.0

使用场景


用户登录云管店应用,此时没有办法直接登录阿里巴巴应用查看数据,或者阿里巴巴数据还未经过处理,不是用户的目标数据。

用户登录云管店(假设该应用对接了阿里巴巴应用的接口)应用,查看自己门店当前的库存数量,同时为了更直观的了解到当前阿里巴巴上挂的店铺的库存,云管店要去访问阿里巴巴接口拉取到该用户在阿里巴巴的店铺的仓库数量,统计成报表。

如果不适用OAuth2.0云管店应该如何读取到阿里巴巴上的库存数量?

image.png

用户提供阿里巴巴账号密码给云管店云管店通过账号密码即可读取到库存信息。那么这么做有带来什么隐患?

  • 阿里巴巴账号密码泄露给云管店云管店可以任意获取用户在阿里巴巴上的数据
  • 云管店数据库如果泄露,也把阿里巴巴的账号密码等数据泄露出去
  • 为了防止云管店任意读取数据,只能通过修改账号密码
  • ...

基于数据开放,且为了保护用户数据安全等诸多问题,OAuth2.0应运而生,并成为当前最主流的解决方案。

OAuth2.0 解决方案


OAuth2.0客户端服务提供商之间,设置了一个授权访问的屏障。客户端无法直接拿到服务提供商的登录账号密码,也就无法直接登录服务提供商,只能请求授权服务提供商

此时会要求用户登录资源提供商(该登录服务由服务提供商提供,不会存在账号密码泄露等问题)。登录后,授权服务提供商提示用户确认授权后提供给客户端一个token令牌。服务提供商根据令牌的时效和授权范围,向客户端开放数据。

image.png

OAuth2.0客户端授权模式


  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式


授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。(本文只提到授权码模式,其他相关客户端授权模式请参考上文的参考资料进行了解)

image.png

流程解析

(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校验合法性。

image.png

授权服务独立一个应用,功能简单,轻量.
资源服务可能由于访问量较大,需要部署多台服务,通过负载均衡来保证服务稳定。

当客户端授权完成并成功拿到token之后即可用它来访问资源服务,拉取数据。那么此时就需要校验token的合法性,那么谁来校验token才是最合适的呢?

资源服务提供者进行token校验

资源服务提供token合法性校验

  • 资源服务需要校验token的合法性,相对复杂
  • 受理了校验token合法性的业务,不能为其他应用提供服务,接口受制。

网管中心进行token校验

网管中心是掌管一切请求的入口,在这一层做token校验也是极为合理的。

  • 就如同需要校验请求是否登录一样,在网管中心校验token
  • 接口不受理校验token合法性的业务,接口可以作为其他服务提供者。
  • 实现相对复杂

授权服务进行token校验

授权服务提供token合法性校验,通过feign将请求再转发到资源服务

  • 把所有与授权相关的处理都统一在一个应用处理。
  • 授权服务的压力甚至比资源服务压力更大,因为所有请求全都要经过授权服务,所以授权服务也需要多台部署。
  • 接口不受理校验token合法性业务,接口可以作为其他服务提供者。

从架构上来说,更加推荐使用网管中心进行token校验,业务方接口方可复用。授权服务进行token检验亦有其优势,业务方接口亦可复用,但是服务压力大。

后记

OAuth2.0 目前已经被各大互联网公司所使用,足以证明它的优秀与不凡。

你可能感兴趣的:(OAuth2.0授权)