Grow

OAuth2.0授权

2018-05-06  本文已影响105人  丑人林宗己

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

参考资料


RFC6794
理解OAuth 2.0

使用场景


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

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

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

image.png

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

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

OAuth2.0 解决方案


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

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

image.png

OAuth2.0客户端授权模式


授权码模式


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

image.png

流程解析

(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

A步骤中,客户端申请认证的URI,包含以下参数:

C步骤中,服务器回应客户端的URI,包含以下参数:

D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

E步骤中,认证服务器发送的HTTP回复,包含以下参数:

基于规范,动手实现一个简易版的授权码模式


对应于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合法性校验,通过feign将请求再转发到资源服务

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

后记

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

上一篇下一篇

猜你喜欢

热点阅读