(精华)2020年9月25日 微服务 OAuth2开放授权

OAuth2

OAuth2是什么

OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息
OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0

Oatuh2用来做什么

有这样一种场景,一个用户(假设是QQ),希望让一个第三方的应用(比如说某个论坛),能够得到关于自身的一些信息(唯一用户标识,比如说QQ号,用户个人信息,比如说是一些基础资料,昵称和头像等)。但是在获得这些资料的同时,却也不能提供用户名和密码之类的验证信息。比如说用户不可能将自身的用户名和密码给第三方让第三方到用户中心之类的地方去获取信息。要达到这样的结果肯定有许多的实现方式。而Oatuh2就是实现上述目标的一种规范,或者说是具体实现的指导方案。

Oauth2具体做法

OAuth定义了四个角色:

  • 资源所有者(resource owner): 能够对受保护资源授予访问权限的实体。当资源所有者是一个人时,它被称为终端用户。
  • 资源服务器(resource server): 托管受保护资源的服务器,能够接受和响应通过令牌对受保护的资源的请求。
  • 客户端(client): 代表资源所有者及其授权进行受保护资源请求的应用程序。术语“客户端”并不暗示任何特定的实现特征(例如,应用程序是在服务器,台式机还是其他设备上执行)。
  • 授权服务器(authorization server): 成功后,服务器向客户端发出访问令牌验证资源所有者并获得授权。

授权服务器和资源服务器之间的交互超出了本规范的范围。授权服务器可以是与资源服务器相同的服务器或单独的实体。单个授权服务器可以发出可以被多个资源服务器接受的访问令牌。

Oauth2的流程

Oauth2,根据RFC6749文档,大致的流程如下图所示

img

上图中的client就是第三方应用。可以看到,Oauth的大致思路是一个线性的流程。

  1. 第三方应用向资源持有者请求获取资源
  2. 资源持有者授权给予第三方应用一个许可
  3. 第三方应用将该许可给予认证服务器进行认证,如果认证成功,返回一个Access Token
  4. 第三方应用使用该access token到资源服务器处获取该access token对应的资源(也就是第一步中资源持有者自身的资源)

在上面的这个流程中,其中第二步,资源持有者如何授权给予第三方应用一个许可就是最为关键的地方。其中RFC6749文档给出4种第三方取得授权许可的方式。

  1. 授权码模式
  2. 简化模式
  3. 密码模式
  4. 客户端模式

其中授权码模式是步骤流程最为详细严谨的一种模式。而网络上大部分的第三方Oauth2实现都是基于授权码模式的。本文也是主要讲解授权码模式中的相关流程性问题。至于其他的三种模式,读者可以自行参看RFC6749文档。

授权码模式

授权码模式的流程如下图所示

img

下面来分别讲解其中的几个点

第三方引导用户跳转至认证服务器的授权页面

在引导跳转的时候需要携带如下的几个参数

  1. response_type:授权类型。授权码模式下,就固定为code
  2. app_id:第三方应用的标识id。
  3. redirect_uri:重定向uri,也就是在授权成功后认证服务器让用户重定向的地址。一般而言也就是当前用户在第三方应用中最初的请求地址
  4. scope:授权范围。可选内容,可以根据第三方应用和实现方的要求自行制定合适的值。
  5. state:透明的验证参数。RFC6749文档推荐认证服务器在重定向的时候应该原封不同的返还这个参数。注意,该参数严格来说应该是一个必须参数。用来防止CSRF攻击。也就是说用于让第三方服务器验证重定向回来的uri的确是认证服务器的行为而不是其他的攻击者伪造的。一般来说跳转到认证服务器的授权页面是走的https,但是认证服务器重定向到回调地址的时候可能走的就是http。此时code存在泄漏以及url存在被伪造的风险。那么第三方应用必须要有办法验证该回调是否的确由认证服务器发起,并且的确是之前自己的授权请求导致的回调。做法其实也不复杂,就是在session中保存一个随机值,作为state参数。认证服务器回调的时候带上该state参数,第三方应用验证该参数是否与自己session中的state参数值一致即可。如果认证的授权页面不是https加密的,那么在发出请求的时候,认证state参数可能会被窃取。那么这个时候还有另外一种做法。也就是第三方应用发送的是加密后的state参数,而认证服务器重定向的时候携带的是解密后的state参数。第三方应用只要在session中判断解密后的值是否与session的一致,也可以达到防止攻击的目的。这样,授权页面也就是可以走在普通的http之中了。

用户选择是否给予授权

这一步是一个用户行为。目前基本的做法都是让用户在授权页面上输入用户名和密码。为了保证安全性,这个页面需要由https来进行保护。当然,如果有其他的方式来保证用户名密码,以及认证的state参数不会泄露也是可以的。如果用户输入正确的用户名和密码,一般就确认为用户给予授权。

认证服务器生成code并且让用户重定向至指定的url

如果用户给予授权,则认证服务器需要生成一个唯一的授权码code。该code的时效性应该比较短,在5分钟以内比较合适。并且该code只能使用一次,下次就会失效。同时,该code与客户端的id,redirect-uri参数是一一对应的关系。认证服务器此时应该让用户重定向至一开始指定的redirect_uri。携带上state和code参数

第三方应用使用code到认证服务器处兑换令牌access token

第三方应用在验证过state参数的正确性后,接着就可以使用code到认证服务器处换取token。这一步,第三方应用需要携带上的参数有

  1. code:就是认证服务器给予的code参数
  2. appid:客户端的唯一标识
  3. redirect_uri:也就是第一步请求中的重定向参数。因为code实际上是与appid和redirect_uri一一对应的。所以用code换取令牌的时候也要携带上这两个参数
  4. grant_type: 授权模式,这里固定为"authorization_code"
  5. appkey:用于验证应用的身份。appid和appkey可以理解为应用自己的用户名和密码。

oauth2的服务器本身都是走https。所以都可以直接明文传输不需要考虑安全性问题。不过如果不是http的,也可以直接参数用户名密码登录的方式,就是给appkey进行md5运算。 关于为何不直接传递accesstoken的问题,是基于安全考虑。因为认证服务器是基于Https,而第三方应用可以是http的。如果在回调的时候直接带上accesstoken,就存在着泄露的问题。

认证服务器返回accesstoken

认证服务器在验证过参数的合法性后,生成一个全局唯一的token,并且返回给第三方应用。返回的内容采用json表示,返回的参数主要有

  1. access_token: 用于获取对应资源的令牌
  2. expires_time: 该令牌的有效期
  3. reflesh_token: 用户获取新的accesstoekn的token。由于accesstoken的有效期比较短,一旦失效,用户需要再走上面的流程是比较繁琐的。为了提升用户体验,可以使用reflesh_token来获取新的accesstoken。不过这个做法,已经有不同的实现方将这个返回参数去掉了。因为实际上reflesh_token也就意味着accesstoekn是永久有效的了。那和直接延长accesstoken的有效期也没有直接区别了。
已标记关键词 清除标记
<div><p>## Proposed Changes</p> <p>Please consider to add support for scenarios where Rabbit Management backend handle both UAA and IdentityServer4 (IS4) OAuth2 token providers. </p> <p>Proposed solution uses OIDC (OpenId-Connect library) client to connect both Rabbit Management UI javascript backend and IdentityServer. In this case OIDC was configured to handle signin/signout redirections, store and management of returned JWT token in browser memory and automatically handle to refresh token when it's about to expire soon.</p> <p>First attempt to integrate Management UI with IdentityServer assumed to obligatory use modified <em>rabbitmq-auth-backend-oauth2</em> pluggin but now it is optional. I will provide manual for both scenarios. This implementation add few more options to <em>rabbitmq_management</em> secrion, but with respect to UAA implementation there are shared parameters for both providers.</p> <p>Below example config has <strong>arrows</strong> for new parameters used in IS4 case:</p> <pre><code> {rabbitmq_management, [ {enable_oauth2, true}, {uaa_client_id, "RabbitMq.ManagementUI"}, {uaa_location, "http://localhost:50000"}, ---> {oauth2_scopes, "read write add delete"}, {oauth2_implementation, identityserver}, </code></pre> <p><em>oauth2_scopes</em> parameter was configured as string <em>oauth2_implementation</em> parameter was configured as enum [uaa , identityserver] with defeault <em>uaa</em> value when its not configured</p> <p>Options like: <em>enable_oauth2</em>, <em>uaa_client_id</em> and <em>uaa_location</em> are shared by UAA and IdentityServer implementations. Their meainng in both cases are the same.</p> <p>Why do I need <em>oauth2_scopes</em> parameter? Analyzed UAA implementation shows that Singular in rpFrame.js has hard coded "openid" scope with whom is requesting to UAA server. I have considered to make Singular use <em>oauth2_scopes</em> as well, but I am not entirely sure about Singular origin, why he's code looks like obfuscated in Rabbit repo and any other but potentailly consequences regardled to UAA.</p> <pre><code> rpFrame.js t=S("openid",h,g); function S(t,n,r){return a.uaaLocation+"/oauth/authorize?response_type=token&scope="+encodeURIComponent(t)+"&client_id="+a.clientId+"&prompt=none&redirect_uri=..... </code></pre> <p>I found that when IdentityServer is requested without any scope assigned to IS4's ApiResource (ApiResource = resource_server_id) it returns token where aud (audience) section hasn't have included information about configured <em>resource_server_id</em> - in another words it has no information that this token is valid for configured <em>resource_server_id</em>.</p> <p><code>eyJhbGciOiJSUzI1NiIsImtpZCI6Ik9YTndkX1RMZGJJY09leDZiTExTdFEiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE1NzU4MTE4NDIsImV4cCI6MTYwNzM2ODc2OCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwMCIsImF1ZCI6IkdpdEh1Yi5SYWJiaXRNcS5NYW5hZ2VtZW50V2Vic2l0ZS5Mb2NhbCIsIm5vbmNlIjoiZmI1NWFjODRjYzEzNDMxZDg5YTFiMGY0YTAyNTE4NTQiLCJpYXQiOjE1NzU4MTE4NDIsImF0X2hhc2giOiJnYnY3RVB4WGJ4d0Q1bU9lVFAtLVFRIiwic19oYXNoIjoiV2dtb05aT3VoUWNsQWJNaW10YlllUSIsInNpZCI6IlBHVnNCM2lDTnFlTkhuYlJ2RGVKb0EiLCJzdWIiOiIxMTEiLCJhdXRoX3RpbWUiOjE1NzU4MTE4MDUsImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.pYBEUUe3ViAY-LDVwB5QP1fW0VFuxH017i16zvLjjDaZKJZVFLxu0EEc0SeNUD-kaifPPzB9d0KlCspptewsoGpFRbPfCFrTUOVl2G18QvW2ENUKMIxtGB_rMQLfeAEn9lYARwr1-W4i-ej_ZC2M00BlfUXiPyEYDGDRziE2z3kYmHODDlhZcrmLsu_BF0GQcaQzUj4HA9-ntw5OwkM9F1md9OhYtAzjIjTqIgkX8h9cw7u_cp_wAIlaQikxNeDQP3KO-IrwW39bkX6z0i3uk8oLOOC9h3bcd01BLMnzfor_UlN8VCZkEDTXF2S_0ywbliNq76w_t2Nnzju_B-Z2Rw</code></p> <p>When such token is provided authorization backend responses (to log) with message:</p> <pre><code> 2019-12-08 14:30:11.233 [warning] <0.874.0> HTTP access denied: Authentication using an OAuth 2/JWT token failed: {invalid_aud, {resource_id_not_found_in_aud,<>, [<>]}} </code></pre> <p>How to tests it? I prepared dedicated solution with preconfigured IdentityServer4. README.md file contains manual how to setup and use it.</p> <p>Case where we do need use any ComplexClaims: (please note that any user will have the same perrmision set)</p> <p>advanced.config</p> <pre><code> {rabbitmq_management, [ {enable_uaa, true}, {uaa_client_id, "GitHub.RabbitMq.ManagementWebsite.Local"}, {uaa_location, "http://localhost:50000"}, {oauth2_scopes, "openid rabbitmq.configure:*/* rabbitmq.read:*/* rabbitmq.write:*/* rabbitmq.tag:monitoring"}, {oauth2_implementation, identityserver} ]}, {rabbitmq_auth_backend_oauth2, [ {resource_server_id, <>}, {key_config, [ {signing_keys, #{ <> => {map, #{ <> => <>, <> => <>, <> => <>, <> => <> }} }} ]} ]} </code></pre> <p>Case where we use ComplexClaims: (please note that each user may have his own permisions set included in <em>extra_scopes_source</em> JWT section)</p> <p>advanced.config</p> <pre><code> {rabbitmq_management, [ {enable_uaa, true}, {uaa_client_id, "GitHub.RabbitMq.ManagementWebsite.Local"}, {uaa_location, "http://localhost:50000"}, {oauth2_scopes, "openid rabbitmq.managementwebsite"}, {oauth2_implementation, identityserver} ]}, {rabbitmq_auth_backend_oauth2, [ {resource_server_id, <>}, {extra_scopes_source, <>}, {key_config, [ {signing_keys, #{ <> => {map, #{ <> => <>, <> => <>, <> => <>, <> => <> }} }} ]} ]} </code></pre> <p>## Types of Changes</p> <p>What types of changes does your code introduce to this project?</p> <ul><li>[ ] Bugfix (non-breaking change which fixes issue #NNNN)</li><li>[x] New feature (non-breaking change which adds functionality)</li><li>[x] Breaking change (fix or feature that would cause existing functionality to not work as expected)</li><li>[ ] Documentation (correction or otherwise)</li><li>[ ] Cosmetics (whitespace, appearance)</li></ul> <h2>Checklist</h2> <p><em>Put an <code>x</code> in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask on the mailing list. We're here to help! This is simply a reminder of what we are going to look for before merging your code.</em></p> <ul><li>[x] I have read the <code>CONTRIBUTING.md</code> document</li><li>[x] I have signed the CA (see https://cla.pivotal.io/sign/rabbitmq)</li><li>[x] All tests pass locally with my changes</li><li>[x] I have added tests that prove my fix is effective or that my feature works</li><li>[ ] I have added necessary documentation (if appropriate)</li><li>[ ] Any dependent changes have been merged and published in related repositories</li></ul><p>该提问来源于开源项目:rabbitmq/rabbitmq-management</p></div>
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页