基于OAuth的统一认证原理解析和实例分析

理解OAuth 2.0


在我们去了解OAuth的原理和分析其实例之前我们先来理解一下Oauth的概念和基本知识:
OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。
本文对OAuth 2.0的设计思路和运行流程,做一个简明通俗的解释,主要参考材料为RFC 6749。

一、应用场景


为了理解OAuth的适用场合,让我举一个假设的例子。

有一个”云冲印”的网站,可以将用户储存在Google的照片,冲印出来。用户为了使用该服务,必须让”云冲印”读取自己储存在Google上的照片。

问题是只有得到用户的授权,Google才会同意”云冲印”读取这些照片。那么,”云冲印”怎样获得用户的授权呢?

传统方法是,用户将自己的Google用户名和密码,告诉”云冲印”,后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。

问题是只有得到用户的授权,Google才会同意”云冲印”读取这些照片。那么,”云冲印”怎样获得用户的授权呢?

传统方法是,用户将自己的Google用户名和密码,告诉”云冲印”,后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。

(1)"云冲印"为了后续的服务,会保存用户的密码,这样很不安全。

(2)Google不得不部署密码登录,而我们知道,单纯的密码登录并不安全。

(3)"云冲印"拥有了获取用户储存在Google所有资料的权力,用户没法限制"云冲印"获得授权的范围和有效期。

(4)用户只有修改密码,才能收回赋予"云冲印"的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。

(5)只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。

OAuth就是为了解决上面这些问题而诞生的。

二、名词定义(角色定义)


在详细讲解OAuth 2.0之前,需要了解几个专用名词。它们对读懂后面的讲解,尤其是几张图,至关重要。

(1) Third-party application第三方应用程序,本文中又称”客户端”(client),即上一节例子中的”云冲印”。

(2)HTTP serviceHTTP服务提供商,本文中简称”服务提供商”,即上一节例子中的Google。

(3)Resource Owner资源所有者,本文中又称”用户”(user)。

(4)User Agent用户代理,本文中就是指浏览器。

(5)Authorization server认证服务器,即服务提供商专门用来处理认证的服务器。

(6)Resource server资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

知道了上面这些名词,就不难理解,OAuth的作用就是让”客户端”安全可控地获取”用户”的授权,与”服务商提供商”进行互动。

三、OAuth的思路


OAuth在”客户端”与”服务提供商”之间,设置了一个授权层(authorization layer)。”客户端”不能直接登录”服务提供商”,只能登录授权层,以此将用户与客户端区分开来。”客户端”登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
“客户端”登录授权层以后,”服务提供商”根据令牌的权限范围和有效期,向”客户端”开放用户储存的资料。

四、运行流程


OAuth 2.0的运行流程如下图,摘自RFC 6749。
这里写图片描述
OAuth运行流程

(A)用户打开客户端以后,客户端要求用户给予授权。

(B)用户同意给予客户端授权。

(C)客户端使用上一步获得的授权,向认证服务器申请令牌。

(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

(E)客户端使用令牌,向资源服务器申请获取资源。

(F)资源服务器确认令牌无误,同意向客户端开放资源。

不难看出来,上面六个步骤之中,B是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。

下面一一讲解客户端获取授权的四种模式。

五、客户端的授权模式


客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

授权码模式(authorization code)

简化模式(implicit)

密码模式(resource owner password 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:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

下面是一个例子。

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

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

code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。

state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
下面是一个例子。
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
          &state=xyz

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

grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。

code:表示上一步获得的授权码,必选项。

redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。

client_id:表示客户端ID,必选项。

下面是一个例子。

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

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

access_token:表示访问令牌,必选项。

token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。

expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。

refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。

scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

下面是一个例子。

 HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 Cache-Control: no-store
 Pragma: no-cache

 {
   "access_token":"2YotnFZFEjr1zCsicMWpAA",
   "token_type":"example",
   "expires_in":3600,
   "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
   "example_parameter":"example_value"
 }

从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存


以上讲解的呢就是我们的Oauth的授权码验证模式,接下来我们来解析一个实例来使用授权码验证模式。

方式认证实例

假设现在有一个应用服务器跑在我本机8000端口,认证服务器在8090端口。在需要用户登录时候把用户带到以下的一个URL.

http://localhost:8090/oauth/authorize?response_type=code&scope=read write&client_id=test&redirect_uri=http://localhost:8000/login&state=09876999

我们注意到几个重要的参数:
这里写图片描述
在这几个参数中分别包含了客户端Id(client_id) 、重定向uri(redirect_Uri)、授权类型(response_type)、申请的权限范围(scope)、客户端当前状态(state)可以是任意值。

response_type:表示授权类型,就是上面讲的那四种类型,这里用的是code方式。

client_id:表示客户端的ID,代表哪个应用请求验证

redirect_uri:表示重定向URI,验证以后的回调地址,一般用来接收返回的code,以及做下一步处理。

scope:表示申请的权限范围,

state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。作为安全校验。

下面是验证服务器接受这个请求的控制器关键代码:

@RequestMapping("authorize")
public void authorize(HttpServletRequest request, HttpServletResponse response) throws Exception {
    try {
         OAuthAuthxRequest oauthRequest = new OAuthAuthxRequest(request);
         if (oauthRequest.isCode()) {
            CodeAuthorizeHandler codeAuthorizeHandler = new CodeAuthorizeHandler(oauthRequest, response);
             LOG.debug("Go to  response_type = 'code' handler: {}", codeAuthorizeHandler);
             codeAuthorizeHandler.handle();
         } else if (oauthRequest.isToken()) {
             TokenAuthorizeHandler tokenAuthorizeHandler = new TokenAuthorizeHandler(oauthRequest, response);
             LOG.debug("Go to response_type = 'token' handler: {}", tokenAuthorizeHandler);
             tokenAuthorizeHandler.handle();
        } else {
             unsupportResponseType(oauthRequest, response);
            }
        } 
    }

首先拿到这个请求以后根据请求的参数将其封装成一个OAuthAuthxRequest,基本就是把请求过来的参数,方法绑定便于使用。这是由oltu提供的OAuthRequest的进一步封装。

然后判断这个请求的授权的类型是否是code,也就是判断下请求参数的response_type是否为code,可以看到目前制作了两种类型的授权。

然后根据对应的授权类型,构造对应的方法处理器。下面是handle的实现接口:

  public void handle() throws OAuthSystemException, ServletException, IOException {
        //验证请求是否合法,主要是针对参数做基本的校验,重定向链接,客户端ID授权范围等这些信息与注册的是否相同。
        if (validateFailed()) {
            return;
        }
        //判断用户是否登录过,根据session判断。因此多个应用使用同一个授权服务的话,是可以直接跳过登录步骤的也就实现了单点登录的效果。如果没有登录的话,这一步的请求会被重定向至登录页面。(登录也得隐藏域会带上这些参数)
        if (goLogin()) {
            return;
        }
        //这个请求如果是从登录页面提交过来的,那么就提交用户的登录,这个框架中交给shiro去做登录相关的操作。
        if (submitLogin()) {
            return;
        }
        // 本系统中把登录和授权放在两个步骤中完成,有点像新浪微博的方式,QQ是一步完成授权。用户未授权则跳转授权页面
        if (goApproval()) {
            return;
        }
       //与登录类似,也是提交用户批准或拒绝了权限请求
        if (submitApproval()) {
            return;
        }
        //以上任意一步没有通过都是授权失败会进行相应处理,如果都通过了就发放Code码。
        handleResponse();
    }

如果以上步骤都通过的话,认证服务器会转向这个会调地址,带上发放的Code码,类似如下:

http://localhost:8000/login?code=bca654ab6133ab3cbc55bb751da93b1c&state=09876999

可以看到带回了返回的参数,以及原样返回的状态码

应用服务器这时候拿到返回的code去换token,发起如下的一个请求:

localhost:8090/oauth/token?client_id=test&client_secret=test&grant_type=authorization_code&code=bca654ab6133ab3cbc55bb751da93b1c&redirect_uri=http://localhost:8000/login&scope=read%20write&state=09876999

与之前请求类似只是多了一个code字段,去验证客户端的合法性。
这里写图片描述

验证服务器会在收到code以后去查找是否有支持这种code的处理器,如果有则发放token。

for (OAuthTokenHandler handler : handlers) {
            if (handler.support(tokenRequest)) {
                LOG.debug("Found '{}' handle OAuthTokenxRequest: {}", handler, tokenRequest);
                handler.handle(tokenRequest, response);
                return;
            }
        }

初始化支持的handler

private void initialHandlers() {
        handlers.add(new AuthorizationCodeTokenHandler());
        handlers.add(new PasswordTokenHandler());
        handlers.add(new RefreshTokenHandler());
        handlers.add(new ClientCredentialsTokenHandler());
    }

验证通过后应用服务器会接受到包含token的一个json数据:

{
"access_token": "23e003b5e4b9b7eda228b845532d8336",
"refresh_token": "d6b49710f398c405a62f31a6676c5830",
"token_type": "Bearer",
"expires_in": 43199
}

这个token是有一定的有效期的,在服务端会缓存这个token以便下一次查询,应用客户端也应该保留这个token,访问受限资源时候需要带上这个token去验证身份。
比如请求一个API如下:

curl -i -X GET \
   -H "Authorization:Bearer 33dbfc80f5659c6fdec73a044ff724c3" \
 'http://localhost:8090/api/test'

资源服务器上使用shiro做安全验证,配置OAuth2对应的realms即可:

<property name="realms">
<list>
    <bean id="systemAuthorizingRealm" class="me.kbiao.example.modules.sys.security.SystemAuthorizingRealm"/>
    <bean id="oAuth2Realm" class="me.kbiao.example..modules.sys.security.OAuth2Realm"/>
</list>
</property>

在这个reamls中根据token去查到用户信息,再去分发对应的资源。
自此便完成了整个oauth2的流程。
这个流程中认证服务系统需要配置三张数据表:

这里写图片描述

client_details表中存放注册的客户端数据。如回调地址,授权类型,是否信任,权限信息等

code中存放发放给客户端应用的code,使用后失效,以保证安全性

access_token中存放用户信息、客户端和token的对应关系。

项目是基于Shiro+ALTU实现,参考方案mkk/oauth2-shiro - 码云 - 开源中国 ,更详细的内容,可以去读读Shengzhao Li开源的代码

最后感谢 大神阮一峰的博客Oauth2理解
以及慕课网博客 http://www.imooc.com/article/10931

猜你喜欢

转载自blog.csdn.net/dengchenrong/article/details/79268866