开发授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片、视频、联系人列表等),而无需将用户名和密码提供给第三方应用。
OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下里的2小时内)访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站他们存储在另外服务提供者的某些特定信息,而非所有内容。
OAuth 是 OpenID 的一个补充,但是完全不同的服务。
OAuth 2.0 是 OAuth 协议的下一版本,但不向下兼容 OAuth 1.0。OAuth 2.0 关注客户端开发者的简易性,同时为Web应用、桌面应用、手机和智慧设备提供专门的认证流程。
参考自阮一峰的理解OAuth的适用场景的例子。
有一个“云冲印”的网站,支持将用户存在各大服务商(包括但不限于百度网盘,Google网盘等)存储的图片打印出来,现在用户想利用“云冲印”来打印他存储在百度网盘的图片。那么问题来了:“云冲印”如何拿到用户存储在百度网盘的图片呢?
答:“云冲印”需要得到用户的授权(即授权“云冲印”读取存储在百度网盘的图片)。传统的方法可能是,用户将自己百度网盘的用户名和密码告诉“云冲印”,后者就可以读取用户的照片了。但这样的做法有以下几个严重的缺点:
- “云冲印”为了后续的服务,“云冲印”需要保存该用户的百度网盘的用户名和密码,这会对“云冲印”公司带来额外的存储开销。
- “云冲印”拥有了用户存储在百度网盘所有自资料的权利,用户没法限制“云冲印”获得授权的范围和有效期。
- 用户修改了百度网盘的密码后,“云冲印”必须也要同步修改密码才能正常使用服务,这很显然是不合理的。
OAuth就是为了解决上述问题而诞生的。
在深入了解OAuth 2.0之前,需要了解几个专用名词。
Third-party application
:第三方应用程序,即上一节例子中的“云冲印”。HTTP service
:HTTP 服务提供商,即上一节例子中的“百度网盘”。Resource Owner
:资源所有者,又称“用户”。User Agent
:用户代理,一般是浏览器。Authorization server
:认证服务器,即服务提供商专门用来处理认证的服务器。Resource server
:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器可以同在一台服务器也可以不同在一台服务器。了解了上述名词,就不难理解,OAuth的作用就是让“客户端”安全可控地获取“用户”的授权,与“服务提供商”进行互动。
OAuth 使得“第三方应用程序”与“服务提供商”之间多了一个授权层(authorization layer)。“第三方应用程序”不能直接登录“服务提供商”,只能访问授权层,以此将用户与“第三方应用程序”区分来。“第三方应用程序”登录授权层所用的令牌,与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
“第三方应用程序”登录授权层后,“服务提供商”根据令牌的权限范围和有效期,向“第三方应用程序”开放用户存储的资料。
OAuth 2.0 的运行流程如下图
A) 用户打开第三方程序后,第三方程序要求用户给予授权。
B) 用户同意给予第三方程序授权。
C) 第三方程序使用上一步获得的授权,向认证服务器申请令牌。
D) 认证服务器对第三方程序进行认证后,确认无误,同意发放令牌。
E) 第三方程序使用令牌,向资源服务器申请获取资源。
F) 资源服务器确认令牌无误,同意向客户端开放资源。
第三方程序必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0 定义了四种授权方式:
基本上遵循 OAuth 协议的第三方程序都是以下流程,举例 A 网站允许 Github 登录,差不多就是以下流程:
以下以gitee
为例,集成OAuth:
访问这个网址完成App登记:https://gitee.com/oauth/applications/new
应用名称随便填,主页URL填http://localhost:8090
,应用回调地址填http://localhost:8090/gitee/oauth/callback
,再随便上传一个图片作为Logo。填写完成后 gitee 会返回供 gitee 识别的该第三方应用的 ClientID 和 ClientSecret 身份识别码。
运行示例代码,然后浏览器输入
http://localhost:8090/gitee/authorize
此时浏览器会重定向到(如果此时你没有登录gitee,则会要求先登录gitee)
https://gitee.com/oauth/authorize?client_id=894c85cea5b36cd629a3cf0867778692acbad55877487e111f4683312bc512cb&redirect_uri=http%3A%2F%2Flocalhost%3A8090%2Fgitee%2Foauth%2Fcallback&response_type=code
该 url 中包含了client_id
:在gitee申请的第三方程序的识别的ID,和redirect_uri
:同意授权后gitee会回调的地址(会携带一个授权码code)以及response_type
指定授权模式,这里是授权码模式即code。
此时你确认是否同意授权以及第三方程序访问的范围。
点击同意后,浏览器则会回调刚填入的redirect_uri
,这里则是
http://localhost:8090/gitee/oauth/callback?code=18ca69350dfb2f139829944845a017cef6bfddec01c2d71e507cbdbf51962b1c
会看到这里会携带一个授权码code,接着就需要利用这个code向gitee申请令牌。
可以看到已经拿到token了。这是因为后端在拿到code后直接向gitee发起申请得到token了并且返回。
此时已经完成了用授权码code申请到了token,接下来就可以使用token获取用户存储在gitee的资源了。
拿到token后具体可以使用哪些API可以参阅文档,文档中有API的method以及可选参数:https://gitee.com/api/v5/swagger,这里演示:获取授权用户的资料(即当前用户的资料):https://gitee.com/api/v5/user 浏览器输入
https://gitee.com/api/v5/user?access_token=5d3b7a6d1602934d12709xxxxx
即可以获取到用户的资料了
恭喜你,你已经可以在第三方程序发起请求使用gitee登录了(表面上的)
接下来将带你了解后端代码具体如何与gitee交互的。
这里以gitee的实现为例,github或者gitlab可以自行参考代码。
// curl -X GET http://localhost:8090/gitee/authorize
// Authorize
// 向服务提供商gitee发起获得授权码的申请
func Authorize(c *gin.Context) {
u, _ := url.Parse("https://gitee.com/oauth/authorize")
values := u.Query()
values.Set("client_id", ClientId)
values.Set("redirect_uri", "http://localhost:8090/gitee/oauth/callback")
values.Set("response_type", "code")
u.RawQuery = values.Encode()
// redirect to -> https://gitee.com/oauth/authorize?client_id={CLIENT_ID}&redirect_uri=http://localhost:8090/gitee/oauth/callback&response_type=code
c.Redirect(http.StatusMovedPermanently, u.String())
return
}
使用GET请求该App的/gitee/authorize
资源,服务端就会将clientId
、redirect_uri
、response_type
拼凑到向gitee发起请求的url上,并重定向到该url。此时浏览器会转到服务提供商(这里是gitee),gitee就会向登录的用户发起提示:是否允许该第三方App访问你的gitee资源,并且还会提示资源访问的范围。
// Callback
// 用户同意授权后
func Callback(c *gin.Context) {
code := c.Query("code")
u, _ := url.Parse("https://gitee.com/oauth/token")
values := u.Query()
values.Set("client_id", ClientId)
values.Set("client_secret", ClientSecret)
values.Set("code", code)
values.Set("grant_type", "authorization_code")
values.Set("redirect_uri", "http://localhost:8090/gitee/oauth/callback")
u.RawQuery = values.Encode()
client := http.DefaultClient
req, _ := http.NewRequest(http.MethodPost, u.String(), nil)
req.Header.Set("accept", "application/json")
res, _ := client.Do(req)
// HTTP POST -> https://gitee.com/oauth/token?client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&code={CODE}&grant_type=authorization_code&redirect_uri=http://localhost:8090/gitee/oauth/callback
defer res.Body.Close()
bytes, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(bytes))
obj := make(map[string]interface{})
json.Unmarshal(bytes, &obj)
c.JSON(http.StatusOK, obj)
return
}
待上述第1步完成后,gitee会携带授权码code回调服务端redirect_uri
的地址/gitee/oauth/callback
。此时服务端会将client_id
、client_secret
、code
、grant_type
、redirect_uri
拼凑到向gitee发起请求的url上,指定接受json格式的返回值,此时返回的json就是一个包含token的对象了。
func Userinfo(c *gin.Context) {
token := c.Query("token")
u, _ := url.Parse("https://gitee.com/api/v5/user")
values := u.Query()
values.Set("access_token", token)
u.RawQuery = values.Encode()
client := http.DefaultClient
req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
req.Header.Set("accept", "application/json")
res, _ := client.Do(req)
// HTTP GET -> https://gitee.com/api/v5/user?access_token={TOKEN}
defer res.Body.Close()
bytes, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(bytes))
info := new(UserInfo)
json.Unmarshal(bytes, &info)
c.JSON(http.StatusOK, info)
return
}
待第2步获取到token后,我们就可以使用该token访问用户存储在gitee的资源了。使用GET请求将token作为query访问/gitee/userinfo
,此时服务端就会将此token拼凑到向gitee请求资源的url上,发起GET请求,制定接受json格式的返回值,此时gitee就会返回该token对应的userinfo了。
https://github.com/FanGaoXS/oauth_demo.git
代码用Go集成了gitee
、github
、 gitlab
的OAuth实现,记得阅读README然后根据实际情况修改.env
文件中的配置信息。
理解OAuth 2.0:https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html