如今的互联网已经渗透了各种各样的生活与业务场景,就像人们在现实生活中从来不是一个简单的个体,我们需要有很多的复杂的关系网络。互联网的应用也是如此,如今很难看到有哪一个应用能做到完全的独立发展而不与其周边发生关系。所以在今天应用都非常讲究自己的生态,合纵连横,彼此之间需要大量的信息共享。这些复杂的关系就带来了一个非常重要的问题:身份认证、资源授权、账号维护。当然还有API的认证访问控制。
举个栗子,在你的日常生活中,你可能需要使用数十个APP, 这些APP各自拥有独立的账号密码,你就需要维护不同的这些账号与密码,你可能为了省事,所有app一套账号密码,于是当应用A被暴库之后,你就被迫把这数十个APP的账号密码全部修改一遍。你也可能会更加在意自己的账号安全,于是你把数十个APP的账号采用一个固定+变化的密码来组合,这很不错,可以帮助缓解很大一部分安全问题,同时又减少自己密码维护的问题(记忆力还好吗),但是这些APP可能不一定如你所愿能够都按照你自己设想的固定+变化的模式,有的可能只支持数字,有的则支持数字加密码,有的则还要求最低长度和更复杂的组合,于是你又开始用小本本记录不同应用的密码格式(嗯,至少曾有那么一阶段我确实是这么干的,用一段描述语言记录在电脑里,当忘记的时候,就去查找电脑里的这个hint)。
有没有更好的方法?
如果你非常信任一家安全做的好、信誉体系也很好的公司,那么是否可以用这一个账号横行互联网?相信我们已经有了答案,在今天,我们可能很多时候已经在这么用了,当你登录某某应用,你习惯的跳过用户注册而是去点击"使用***登录",在弹出的界面里非常神圣的点下"同意"。于是你不再需要去记忆那么多的应用账号。这其实就是典型的开放式授权(Open Authorization)简称oAuth(当前版本为2,也称oAuth2)。
额,你好像在骗我,你说了那么多好像都是关于验证,怎么这里又说是授权Authorization。是的,你说的没错,不过我也没有骗你,只是这里有一些傻傻不好分清的问题,这个oAuth设计的本意是用来解决垮利益团体之间的数据访问问题,就像我们开篇说的那样,不同的公司的应用之间有大量数据的相互访问问题,比如 A公司开发了一个在线照片打印应用,可是这个公司并不经营照片存储服务,你的照片可能存在B、C、D这些不同公司的网盘上(是的,早期为了占便宜,我确实占了好多公司的网盘,后来他们有些网盘耍流氓发个通知就说不干了。。,算了反正是白piao,还好我采用的是RAID1), 所以这就带来了问题,你怎么把照片给这个在线打印公司,从BCD网盘下载下来发给他们?把网盘的账号密码给打印公司?显然这些方式都不行,如果靠下载上传,估计你就懒得弄了,如果是给账号密码,除非你不清醒。
而对于打印公司以及网盘服务商来说,他们也有类似的烦恼,如果让用户上传下载那用户体验太糟糕,而且还有维护一整套这样系统,所以打印公司更希望有一个简便的方式同时对接BCD网盘公司,只要这些网盘用户的一个同意,那么自动去从这些网盘上把用户照片拉下来打印,自己0库存。而对BCD网盘公司来说,单单存储冷冷的数据显然不是目的,何况你还是在白piao,必须得搞花样,所以网盘公司也很想对接这些各种打印图片的公司,但是对他们来说必须要解决用户对账号以及照片的安全问题。
所以可以看出,在这三个不同利益体来说,都有渴望有一种东西来同时解决他们的问题,这就是授权,当用户希望打印照片时候,打印公司引导客户进入网盘界面,用户是登录网盘并授权网盘容许将我的哪些资源共享给照片打印公司,比如把自己美颜过的照片共享去打印,那些原始照片则不容许打印公司访问到,这样就非常安全了。所以可以总结一下:
oAuth就是用于解决这样的场景,所以你可以看出这里面是一个授权的过程。可你说了半天还没说为什么一开始都是扯的认证,hmmm,毕竟我也是花了不少时间去学习的,讲完也是要个过程,就像这个文章,它是一个系列,要分以下几篇才能讲完:
1. NGINX在oAuth中作为(Client)角色与资源服务角色代理,Authorization code模式(借助oAuth proxy服务)--本篇
2. NGINX在oAuth中作为(Client)角色与资源服务角色代理,Authorization code模式(不借助oAuth proxy服务)
3. NGINX在OIDC中验证与识别id_token (Implicit 模式)
4. NGINX在oAuth作为资源服务角色,代理认证与身份信息识别(Token introspection)
那为啥这里会扯到认证呢,实际上你会发现,在授权的过程中,身份显然是逃脱不掉的,授权必然基于某个用户,所以在oAuth规范中并没有强调你不能拿这个做除了授权之外的事情,再加上有时候,我们也确实不是需要授权的场景,而是希望减少自己的账户维护,用一家公司的账号登录很多其他公司的产品,所以就有了很多把oAuth用于认证的场景,当然正是由于oAuth没有对认证做出很多规范的定义, 导致不同公司的程序在实现验证时候就出现了各种不同的设计,没有一个标准的方式去获取用户信息,没有一个通用标准的scope,基于此就又出现了OpenID Connect(OIDC), OIDC是基于oAuth的,其几方的通信流程是一样的,差异在于OIDC在向IdP(存储有账号并执行验证的一方)发起请求时候在scope里会带上openid标记,最后IdP返回的信息里也会除了oAuth正常的access_token之外,还会携带一个ID token(JWT),应用拿到这个ID token之后就可作为登录使用,如果还需要更多额外信息则可以拿着access_token再去userinfo endpoint去获取更多用户信息。此外,OIDC是一个协议族,还包含其它很多规范,比如session管理,注册发现等。由于通信机理上oAuth与OIDC极其相似,因此我们常常会混淆这两者,常常会说oAuth认证,实际上应该是oAuthZ,而OIDC才是oAuthN。
回到本篇,在本篇中,我们将按照一个标准的oAuth authorization code模式的交互来看NGINX在这里面能帮助用户做到什么,为什么需要NGINX去做这样的事情。
首先我们需要先理清oAuth authorization code模式下的整个交互过程,为了避免直接说RFC的晦涩,我们假设一个场景。
你是在一个创业公司,比如搞AI、大数据什么的公司(当然还没上市,上市了,估计你也没时间看这文章了),你们公司大量的使用云的服务例,买服务器搞机房,搞基础设施,那是没有的事。你们拿开源搞了很多系统,快速的就把业务上线了,大家知道开源系统有很大的一个特点,对开发者友好,什么意思呢,就是怎么方便怎么来(就是开发者懒的意思,非要直说。。),所以你看到很多开源的系统都没怎么考虑认证这些,装好就访问了,好似没有账号认证是理所当然的一样,一开始,这并没什么,因为就你一人,啥事你都干,随着系统越来越多,员工也开始变多,你开始需要对不同的人访问不同的系统要做出一些限制,而且你还准备上市呢,作为一家上市公司,你们系统一个账号都没有,那说不过去的。然后你还有一些应用开发系统需要对接github的API,你需要只让部分高级开发者才能访问某个私有repo。而你么自己又没有时间去自己搭建一套新的用户管理体系,幸运的是,这些人都有github账号,所以可以利用这些github账号来做是最简单快速的事情,这些需求可以总结为:
- 需要在不同的系统上实现一个功能,让这些系统能够对接github,使用github账号来决定员工是否可以访问某个系统
- 在应用开发系统上也用github账号登录,并向github申请资源授权中包含此人的repo等信息,如果此人无私有repo权限,自然应用开发系统在此员工的权限下也就无法获取到私有repo内容
这些需求oAuth正好可以帮助解决,但有个问题,如果加入oAuth机制,就需要在系统上进行开发,这么多的开源系统,开发的语言不一样,甚至有的系统还不敢贸然再开发,要想实现其实难度以及工作量实际非常大。
在看NGINX能做什么之前,先来看看没有NGINX下,上述需求的oAuth过程。
从上面的过程可以看到对用户来说就是在github上登录并做一次授权,而浏览器做了两次跳转,真正有用的access_token是后端应用服务器与github之间的事情。用户自己以及浏览器本身并不能看到这个access_token的内容,这就是所谓的backend channel,相对比较安全。那么应用拿到这个access_token之后干嘛呢?
-如果仅限于获取用户的一些基本信息,而且返回的access_token是JWT的,那么应用服务器是自己可以获取到JWT里的内容了,这样将用户信息抽取和本地的一些用户ID进行关联,就可以作为登录使用了(当然如果是纯身份认证和这种联合登录场景,实际上还是应该考虑OIDC)。 当然如果这里的access_token是opaque,那应用服务器还需要去做token introspection,就是需要和授权方再次核验之后才能使用相关信息。
-如果不仅仅是限于获取用户信息,而是要去获取额外资源,比如还需要去获取这人的repo内容,那么应用服务器就需要拿着这个access_token去访问一个github的repo资源服务器(资源服务器和授权服务器不一定是同一个,大型的场景也往往一般不是同一个)来获取此人的repo内容,那么上图就变成了这样:
所以,你会发现,这里面的web应用后端是非常关键的,它参与了整个oAuth过程并最终获取到了access_token,试想一下,就像开篇说的你这个公司,很多开源的用不同语言开发的系统,你都要改造加入这个能力。而你此时实际上只是希望根据用户的信息决定这个系统必须首先通过oAuth过程登陆才可以被访问,或者这个系统根据用户名决定谁能访问。
这个工作其实可以通过在web应用后端的前面放置NGINX来实现,也就是说让NGINX来实现代表后端应用参与到oAuth的认证过程,然后NGINX可以根据access_token决定是否放行,或者拒绝某些用户,或者将用户信息透传给后端应用做更多处理。
仔细观察上面的整个验证流程,这就需要NGINX能够参与到去构造跳转返回,凭借authorization code构造请求去直接访问github授权服务器。这些工作如果是单纯在NGINX上做,其实是很难的,通过njs开发是一种方式但需要有具备对JWT进行认证的能力(因此NGINX Plus可以不需要像本文demo中安装一个oauth proxy服务,直接利用njs模块+KV模块+JWT模块即可实现,详细可参考本系列第二篇),但实际上可以借助auth_request的能力配合一个oAuth proxy来实现,也就是说我们把本来需要在各种不同开源系统上都去打造的oAuth验证过程的实现代码给它抽象出来,弄一个通用的,由这个oAuth proxy代理参与到这个oAuth过程,最后将获得的access_token解析出来里面相关claims信息返给NGINX,NGINX将根据这些信息来控制是否容许访问一个资源,亦或将相关用户信息透传给最终应用。所以其实现逻辑如下:
实现的思路和原理(下面序号和图中无关):
- 配置NGINX来发布被保护的应用
- 在相关应用的location段落下配置auth_request
- 这样当请求到达NGINX后,NGINX将会由auth_request发起子请求认证
- 子请求将会被proxy_pass给oauth proxy服务的一个接口
- 根据auth_request的特点,需要oauth proxy返回相关status code来指示NGINX是放行还是返回401
- 所以oauth proxy收到子请求后会判断该用户是否之前已经完成了相关oauth认证工作,如果该用户没有登录过,或者有效期已过,那么oauth proxy返回401 (这里是靠用户浏览器是否携带oauth proxy颁发给的一个cookie信息来检查的)
- NGINX截取401状态,通过定义error_page实现如果返回了401则发送302跳转给用户浏览器,这个跳转的地址实际oauth proxy的一个专门接口用于触发后续oAuth过程,此后的过程与正常oAuth无差异
- oAuth proxy完成整个oAuth过程后,返回一个302跳转给用户端浏览器,这个返回里还会携带返回相关cookie,让其重新访问被保护的应用
- NGINX收到请求后,再次触发auth_request,auth_request再次发送请求给oauth proxy的一个接口,这次访问携带了8中的cookie,这样oauth proxy根据cookie知道是谁,并解析相关其access_token,将相关claims通过放在响应头里返回给NGINX
- 利用auth_request_set将子请求中的响应header里的claims提出放到变量,传递给父请求
- NGINX根据这些变量判断是否放行,或者将这些用户信息再放到请求header里将内容传递给最后被保护的应用
这样的oauth proxy网上有多种实现,这里简单列举:
vouch-proxy
oauth2 proxy by lua(直接在lua里实现proxy,无需额外安装proxy服务)
Demo演示
本次演示采用NGINX plus 与 vouch-proxy来实现, 关于vouch-proxy的具体安装与配置,请直接参考其github,并不复杂
在实际demo中web应用后端实际也是有中间的NGINX来模拟,利用return返回内容。
NGINX 配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
############entry for protected app http://authcode.cnadn.net/personalinfo server { listen 80; server_name authcode.cnadn.net; #root /var/www/html/; # send all requests to the `/validate` endpoint for authorization auth_request /validate; #此location仅供auth_request的子请求调用 location = /validate { # forward the /validate request to Vouch Proxy proxy_pass http://127.0.0.1:9090/validate; # be sure to pass the original host header proxy_set_header Host $http_host; # Vouch Proxy only acts on the request headers proxy_pass_request_body off; proxy_set_header Content-Length ""; # optionally add X-Vouch-User as returned by Vouch Proxy along with the request auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user; # these return values are used by the @error401 call auth_request_set $auth_resp_jwt $upstream_http_x_vouch_jwt; auth_request_set $auth_resp_err $upstream_http_x_vouch_err; auth_request_set $auth_resp_failcount $upstream_http_x_vouch_failcount; } # if validate returns `401 not authorized` then forward the request to the error401block error_page 401 = @error401; location @error401 { # redirect to Vouch Proxy for login return 302 http://vouch.cnadn.net/login?url=$scheme://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err; # you usually *want* to redirect to Vouch running behind the same Nginx config proteced by https # but to get started you can just forward the end user to the port that vouch is running on } # 负责真实的被保护应用 location / { # forward authorized requests to your service protectedapp.yourdomain.com ##这个后端应用实际也由这个nginx来模拟 proxy_pass http://127.0.0.1:8080; # you may need to set these variables in this block as per https://github.com/vouch/vouch-proxy/issues/26#issuecomment-425215810 auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user; auth_request_set $auth_resp_x_vouch_idp_claims_avatar $upstream_http_x_vouch_idp_claims_avatar_url; auth_request_set $auth_resp_x_vouch_idp_claims_company $upstream_http_x_vouch_idp_claims_company; auth_request_set $auth_resp_x_vouch_idp_claims_blog $upstream_http_x_vouch_idp_claims_blog; # set user header (usually an email) proxy_set_header X-Vouch-User $auth_resp_x_vouch_user; # optionally pass any custom claims you are tracking proxy_set_header X-Vouch-IdP-Claims-company $auth_resp_x_vouch_idp_claims_company; proxy_set_header X-Vouch-IdP-Claims-avatar $auth_resp_x_vouch_idp_claims_avatar; proxy_set_header X-Vouch-IdP-Claims-blog $auth_resp_x_vouch_idp_claims_blog; } } |
后端应用的模拟配置
1 2 3 4 5 6 7 8 9 10 11 12 |
server { listen 8080; location /personalinfo { default_type text/html; set $user $http_x_vouch_user; set $avatar $http_x_vouch_idp_claims_avatar; set $company $http_x_vouch_idp_claims_company; set $blog $http_x_vouch_idp_claims_blog; return 200 '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><h2>Your personal info:</h2><hr />Name: $user <br>avatar: $avatar <br>company: $company <br>blog:$blog </html>'; } } |
负责接收客户端浏览器发起到oauth proxy的请求配置:
1 2 3 4 5 6 7 8 9 10 |
#######work for vouch login/auth server { listen 80; server_name vouch.cnadn.net; location / { proxy_pass http://127.0.0.1:9090; # be sure to pass the original host header proxy_set_header Host vouch.cnadn.net; } } |
访问的效果过程:
首次访问http://authcode.cnadn.net/personalinfo,浏览器被自动跳转到vouch.cnadn.net/login?接口上,这个跳转实际是NGINX驱动出来的
vouch.cnadn.net收到后进行处理,并让浏览器跳转github.com/authorize接口,因为也没有在github登录过,所以github又跳转到/login接口让用户登录
就出现了登录界面,在登录后会显示授权,点击授权将会被跳转到vouch.cnadn.net(oauth proxy的服务地址),这其实返回authorization code给oauth poxy服务。
在点击授权后,浏览器会被继续跳转,github的实现会有下面这个跳转提示,这实际是要浏览器跳转给vouch.cnadn.net的回调接口:
vouch.cnadn.net的回调接口被访问后,会驱动vouch在服务器侧发起access_token的获取,这个时候浏览器端是无法抓取到的。当vouch在服务器获取完毕后,再次返回一个302给浏览器,这个302是要求浏览器正式访问应用地址了,同时伴有相关cookie送到客户端浏览器:
最终就完成了访问:
总结:
利用NGINX的auth_request功能,并通过巧妙的配置来借助oauth proxy实现oAuth的完整验证过程,并将相关用户信息传递给NGINX实现访问控制与信息处理。免除了后端所有应用都需要开发代码来实现oauth的验证,使得企业可以快速的借助第三方账号来控制用户访问
后续
在本次实践中,是采用oAuth的authorization code模式,并借助了外部的oauth proxy服务。如果不希望借助外部服务,希望在单纯的NGINX上实现,则可以参考本系列的第二篇。
文章评论