1.概念
oauth2是一种认证授权协议,具体来说就是资源拥有者授权第三方应用使用安全的方式访问用于资源信息(用户名称,图像等),从而无需将用户名和密码提供于第三方应用。
2.oauth2认证授权分为四种模型
*不管哪种模式,再申请令牌之前,都必须在资源系统进行备案,说明身份(在访问之前,第三方申请授权码是需要携带自己的client_d和client_secret),这样资源服务器会拿到这两个身份的识别,如此做法是为了防止令牌被滥用。如果没进行备案识别身份的第三方应用,是无法拿到令牌的。
2.1授权码模式
该模式是通过向访问资源服务,之后用户收到授权请求,如果授权同意,给第三方返回一个授权码,之后第三方应用通过该授权码去获取令牌,最后再使用令牌获取对应资源服务获取对应的资源信息。
第一:A网站提供一个链接,用户点击后会跳转至B网站,授权用户数据给A网站使用。如下URL:
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
如上url中:
response_type :表示要求返回授权码(code)
client_id:网站A的客户端id,让B网站知道是谁在请求
redirect_url : 该参数表示B接受或者拒绝请求后的跳转网址
scope:表示请求的授权范围
如下图:
第二:用户跳转后,B网站会要求用户登陆,之后发送请求给用户是否同意授权给A网站权限,用户表示同意后,这时B网站会跳回redirect_url,指定的参数网址并且跳转回时,会传递一个授权码,如下:
https://a.com/callback?code=AUTHORIZATION_CODE
如下如:
第三:A网站拿到授权码之后,就可以在后台向B网站请求令牌。
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL
如上参数:
client_id和client_secret参数是用来让B确认A身份的标识,(client_secret 是保密的,仅后台请求),grant_type:authorization_code表示使用了授权码方式,code:是上一步获取的授权码,redircet_url:该参数是颁发令牌之后的回调地址。
第四:B网站收到请求后,验证身份跟授权码无误,会颁发令牌信息,做法就是在redirect_uri指定的网址发送一段json数据,具体内容如下:
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}
如上的json中的access_token 就是颁发的token信息,A网站拿到后,通过token信息,就可以向已经授权的资源的B网站访问相关的用户信息了。
该处只模拟授权方式,因为几乎所有的使用的都是授权码,如下三方,server,third,user,:
server.go:
package main
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type TokenInfo struct {
Access_token string `json:"token"`
Token_type string `json:"token_type"`
Expires_in uint64 `json:"expires_in"`
Refresh_token string `json:"refresh_token"`
Scope string `json:"scope"`
Uid uint64 `json:"uid"`
Info string `json:"info"`
}
type User struct {
Name string `json:"name"`
Tel uint32 `json:"tel"`
}
var ClientIdMap map[string]interface{} // 存储请求的客户端id,client_id:client_secret
var TokenMap map[string]*TokenInfo // 请求的token
func init() {
ClientIdMap = make(map[string]interface{}, 0)
TokenMap = make(map[string]*TokenInfo)
}
func main() {
ser := gin.Default()
// 第三方跳转回认证用户并让用户授权
ser.POST("/auth", func(ctx *gin.Context) {
fmt.Println("third app jump current server .... ")
clientId := ctx.Query("client_id")
ClientIdMap[clientId] = "client_secret"
// 内部跳转
ctx.Request.URL.Path = "/login"
ser.HandleContext(ctx)
})
// 模拟用户登陆信息的输入,该处默认已经进行输入用户登陆认证
ser.POST("/login", func(ctx *gin.Context) {
// 该处就省略当前服务跟用户点击是否授权的操作过程,默认验证成功,重新跳回第三方网页,并携带code
// https://a.com/callback?code=AUTHORIZATION_CODE
AUTHORIZATION_CODE := uuid.New().String() // 颁布一个授权吗code,供给第三方使用
url := ctx.Query("redirect_uri") // 跳转回定向之前传递的url
url += "?" + "&code=" + AUTHORIZATION_CODE
ctx.Redirect(http.StatusPermanentRedirect, url)
})
// 获取token
ser.POST("/access_token", func(ctx *gin.Context) {
// 验证身份
clientId := ctx.Query("client_id")
if _, ok := ClientIdMap[clientId]; !ok {
fmt.Println("client is error")
ctx.JSON(200, gin.H{"msg": "request token is failed"})
return
}
// 密钥认证
secret := ClientIdMap[clientId]
if secret != "client_secret" {
fmt.Println("secret is error")
ctx.JSON(200, gin.H{"msg": "secret auth is failed"})
return
}
// 模拟生成token,并向第三方发送token相关信息json结构
tk := uuid.New().String()
token := &TokenInfo{
Access_token: tk,
Token_type: "bear",
Expires_in: uint64(time.Second * 86400),
Scope: "read",
Uid: uint64(rand.Int() % 1000),
Info: "",
}
// 存储当前客户端对应的token信息,便于后续通过token获取其他信息认证身份
TokenMap[clientId] = token
// 向重定向uri发送token信息
tokenUrl := ctx.Query("redirect_uri")
client := http.Client{}
tokenData, _ := json.Marshal(token)
tokenSendReq, err := http.NewRequest("POST", tokenUrl, strings.NewReader(string(tokenData)))
if err != nil {
ctx.JSON(http.StatusOK, gin.H{"error": "response token is failed"})
return
}
response, err := client.Do(tokenSendReq)
if err != nil {
ctx.JSON(http.StatusOK, gin.H{"error": "req send token info is failed"})
return
}
defer response.Body.Close()
ctx.JSON(200, gin.H{"success": "success to send token info"})
return
})
// 获取用户信息
ser.POST("/user_info", func(ctx *gin.Context) {
// 验证身份id
clientId := ctx.Query("client_id")
if _, ok := TokenMap[clientId]; !ok {
ctx.JSON(200, gin.H{"msg": "token is error"})
return
}
// 验证密钥
secret := ctx.Query("client_secret")
if secret != "client_secret" {
ctx.JSON(http.StatusOK, gin.H{"error": "client secret auth is failed"})
return
}
// 验证token
token := ctx.Query("access_token")
if TokenMap[clientId].Access_token != token {
ctx.JSON(http.StatusOK, gin.H{"error": "token auth is failed"})
return
}
// 此处可以扩展为从数据库获取数据
user := &User{
Name: "hello",
Tel: 65890,
}
ctx.JSON(200, user)
return
})
ser.Run(":8080")
}
third.go:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type TokenInfo struct {
Access_token string `json:"token"`
Token_type string `json:"token_type"`
Expires_in uint64 `json:"expires_in"`
Refresh_token string `json:"refresh_token"`
Scope string `json:"scope"`
Uid uint64 `json:"uid"`
Info string `json:"info"`
}
type User struct {
Name string `json:"name"`
Tel uint32 `json:"tel"`
}
// 第三方客户端id
var client_id string
var client_secret string
var UserMap map[string]*User // 存储授权获取的用户信息
func init() {
client_id = uuid.New().String() // 当前客户端的client_id
client_secret = "client_secret" // 客户端的密钥
UserMap = make(map[string]*User, 0)
}
func main() {
thd := gin.Default()
// 用户访问,重定向到到对应的服务上
thd.POST("/login", func(ctx *gin.Context) {
// https://b.com/oauth/authorize?
// response_type=code&
// client_id=CLIENT_ID&
// redirect_uri=CALLBACK_URL&
// scope=read
url := "http://127.0.0.1:8080/auth?" + "client_id=" + client_id +
"&redirect_uri=" + "http://127.0.0.1:8081/getCallBack" + "&scope=read"
// 用户访问,直接重定向到信息服务
ctx.Redirect(http.StatusPermanentRedirect, url)
return
})
// 重定向回来之后,向server申请code
thd.POST("/getCallBack", func(ctx *gin.Context) {
AUTHORIZATION_CODE := ctx.Query("code") // 获取授权吗code
// 并且向B请求令牌token
// https://b.com/oauth/token?
// client_id=CLIENT_ID&
// client_secret=CLIENT_SECRET&
// grant_type=authorization_code&
// code=AUTHORIZATION_CODE&
// redirect_uri=CALLBACK_URL
// 利用code请求token
client := http.Client{}
url := "http://127.0.0.1:8080/access_token?" + "client_id=" + client_id + "&client_secret=" + client_secret +
"&grant_type=authorization_code" + "&code=" + AUTHORIZATION_CODE + "&redirect_uri=" + "http://127.0.0.1:8081/getToken"
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return
}
response, err := client.Do(req)
if err != nil {
return
}
defer response.Body.Close()
ctx.JSON(http.StatusOK, gin.H{"msg": "success send to get token info"})
})
// 接收服务认证授权后发送的token信息
thd.POST("/getToken", func(ctx *gin.Context) {
tk := &TokenInfo{}
if err := ctx.ShouldBindJSON(tk); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "parse token is failed"})
return
}
client := &http.Client{}
getUserUrl := "http://127.0.0.1:8080/user_info?" + "client_id=" + client_id +
"&client_secret=" + client_secret + "&access_token=" + tk.Access_token
getUserReq, err := http.NewRequest("POST", getUserUrl, nil)
if err != nil {
ctx.JSON(http.StatusOK, gin.H{"err": "create get user info req is failed"})
return
}
response, err := client.Do(getUserReq)
if err != nil {
return
}
defer response.Body.Close()
user := &User{}
data, _ := ioutil.ReadAll(response.Body)
json.Unmarshal(data, user)
UserMap[user.Name] = user // 存储一份用户信息
fmt.Println("user = ", user)
// user = &{hello 65890}
// 注:response响应包,只能读取一次,二次读取是读取不到数据的,如果想二次读取,可以利用如下ddd,重新再次构造一个数据区域,重新读取
// second, _ := ioutil.ReadAll(response.Body)
// fmt.Println("second user = ", second)
// second user = []
// 在构造一份数据
// ddd := ioutil.NopCloser(bytes.NewBuffer(data))
// ppp, _ := ioutil.ReadAll(ddd) // 重新构造一份数据区,供给重新读取
// fmt.Println("ppp = ", string(ppp))
ctx.JSON(http.StatusOK, gin.H{"msg": "success to get user info"})
})
thd.Run(":8081")
}
user.go:
package main
import (
"net/http"
)
func main() {
client := http.Client{}
url := "http://127.0.0.1:8081/login"
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return
}
// 点击第三方登陆,第三方返回一个让访问
response, err := client.Do(req)
if err != nil {
return
}
defer response.Body.Close()
}
如上三方,通过user中的方法模拟,要受用某一方账号登陆第三方网站的模拟过程,可以模拟通过,最终third方会获取对应的用户信息。
2.2 凭证式
该种方式适用于没有前端的命令行应用。
第一:A应用通过命令行向B应用发出请求
https://oauth.b.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
grant_type:
参数client_credentials
表示采用凭证式,client_id
和client_secret
用来让 B 确认 A 的身份。
第二:B网站通过认证后,直接给返回令牌。
该种方式针对的是第三方应用,不是单个用户,多用户共享同一个令牌。
2.3 密码式
该方式就是,直接将账号跟密码告诉第三方(前提是高度信任该应用)
第一:A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。
https://oauth.b.com/token? grant_type=password& username=USERNAME& password=PASSWORD& client_id=CLIENT_ID
上面参数:grant_type
参数是授权方式,这里的password
表示"密码式",username
和password
是 B 的用户名和密码。client_id用来B确认A的方式。
第二:B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。
这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
2.4 隐藏式
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。
第一步,A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。
https://b.com/oauth/authorize? response_type=token& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
上面 URL 中,response_type
参数为token
,表示要求直接返回令牌。
第二步,用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri
参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。
https://a.com/callback#token=ACCESS_TOKEN
上面 URL 中,token
参数就是令牌,A 网站因此直接在前端拿到令牌。
注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
2.5 令牌的使用:
A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。
此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization
字段,令牌就放在这个字段里面。
curl -H "Authorization: Bearer ACCESS_TOKEN" \ "https://api.b.com"
上面命令中,ACCESS_TOKEN
就是拿到的令牌。
2.6 令牌的更新
令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。
具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
https://b.com/oauth/token? grant_type=refresh_token& client_id=CLIENT_ID& client_secret=CLIENT_SECRET& refresh_token=REFRESH_TOKEN
上面 URL 中,grant_type
参数为refresh_token
表示要求更新令牌,client_id
参数和client_secret
参数用于确认身份,refresh_token
参数就是用于更新令牌的令牌。
B 网站验证通过以后,就会颁发新的令牌。