密码模式 (Resource Owner Password Credentials)
密码模式就是用户直接将用户名密码提供给客户端,客户端使用这些信息到认证服务器请求授权。具体流程如下:
+----------+ | Resource | | Owner | | | +----------+ v | Resource Owner (A) Password Credentials | v +---------+ +---------------+ | |>--(B)---- Resource Owner ------->| | | | Password Credentials | Authorization | | Client | | Server | | |<--(C)---- Access Token ---------<| | | | (w/ Optional Refresh Token) | | +---------+ +---------------+ Figure 5: Resource Owner Password Credentials Flow
- (A) 资源所有者提供用户名密码给客户端;
- (B) 客户端拿着用户名密码去认证服务器请求令牌;
- (C) 认证服务器确认后,返回令牌;
- 在B中客户端发送的请求中,需要包含这些参数:
参数名称 | 参数含义 | 是否必须 |
grant_type | 授权类型,此处值为password 。 |
必须 |
username | 用户名。 | 必须 |
password | 用户的密码。 | 必须 |
scope | 权限范围。 | 可选 |
如:
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=password&username=johndoe&password=A3ddj3w
- 在C中,认证服务器返回访问令牌。如:
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" }
客户端模式 (Client Credentials)
客户端模式,其实就是客户端直接向认证服务器请求令牌。而用户直接在客户端注册即可,一般用于后端 API
的相关操作。其流程如下:
+---------+ +---------------+ | | | | | |>--(A)- Client Authentication --->| Authorization | | Client | | Server | | |<--(B)---- Access Token ---------<| | | | | | +---------+ +---------------+ Figure 6: Client Credentials Flow
- (A) 客户端发起身份认证,请求访问令牌;
- (B) 认证服务器确认无误,返回访问令牌。
- 在A中,客户端发起请求的参数有:
参数名称 | 参数含义 | 是否必须 |
grant_type | 授权类型,此处值为client_credentials 。 |
必须 |
scope | 权限范围。 | 可选 |
如:
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=client_credentials
2) 认证服务器认证后,发放访问令牌,如:
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, "example_parameter":"example_value" }
实现OAuth 2.0
上面我们一起了解了OAuth 2.0
的流程,现在我们开始实现OAuth 2.0
。
首先,我需要一个使用场景,大概是这样的。
我有一个需要登录的应用(web app)。 用户可以选择使用github登录。 登录后,显示一个简单的欢迎页面。
有了这个场景后,我们开始设计(这里我们实现的是第一种授权码模式)。
登录页面
在这里,我们充当的角色就是client
,我们需要一个简单的页面,让用户选择使用github
登录。
这是一个简单的html页面,public/index.html
页面。
<!DOCTYPE html> <html> <body> <a href="https://github.com/login/oauth/authorize?client_id=89ac6f58f15f658d8dd5&redirect_uri=http://localhost:8080/oauth/redirect"> Login with github </a> </body> </html>
也就是当用户点击Login with github
会访问
https://github.com/login/oauth/authorize? client_id=89ac6f58f15f658d8dd5 &redirect_uri=http://localhost:8080/oauth/redirect
其中:
https://github.com/login/oauth/authorize
是GitHub
的OAuth
网关地址。client_id=89ac6f58f15f658d8dd5
这个是我申请的客户端ID,
需要到github.com/settings/ap…注册。
注册过程很简单,但要注意,最后一栏Authorization callback URL
是和下面redirect_uri
一致。
如:
成功后你会获的Client ID
与Client Secret
, 前者就是这里的client_id
,两者在后面的编码中需要用到。redirect_uri=http://localhost:8080/oauth/redirect
当用户确认,获取权限后重定向到的该地址。
然后我们写个mian.go
, 启动一个简单的服务。
func main() { fs := http.FileServer(http.Dir("public")) http.Handle("/", fs) http.ListenAndServe(":8080", nil) }
写到这里,我们代码已经能跑起了,如果你点击Login with github
我们会看到授权页面。
当我们确定过后,会重定向到我们指定的地址,而且带上code
,如:
http://localhost:8080/oauth/redirect?code=260f17a7308f2c566725
此时我们需要做的是,拿着该code去请求访问令牌(access_token),对应授权码模式中的D步骤。
重定向路由
现在我们有code
了,我们需要去请求令牌。接下来我们就需要向https://github.com/login/oauth/access_token
发送POST请求获取访问令牌(access_token)。
关于更多
GitHub
重定向URI的信息你可以看这里。
现在,让我们将/oauth/redirect
路由补全,在该步骤中请求访问令牌(access_token)。
package main import ( "encoding/json" "fmt" "log" "net/http" ) // 你在注册时得到的 const ( clientID = "你的客户端ID" clientSecret = "你的客户端密钥" ) var httpClient = http.Client{} type OAuthAccessResponse struct { AccessToken string `json:"access_token"` } func main() { fs := http.FileServer(http.Dir("public")) http.Handle("/", fs) http.HandleFunc("/oauth/redirect", HandleOAuthRedirect) http.ListenAndServe(":8080", nil) } // HandleOAuthRedirect doc func HandleOAuthRedirect(w http.ResponseWriter, r *http.Request) { // 首先,我们从URI中解析出code参数 // 如: http://localhost:8080/oauth/redirect?code=260f17a7308f2c566725 err := r.ParseForm() if err != nil { log.Printf("could not parse query: %v", err) w.WriteHeader(http.StatusBadRequest) } code := r.FormValue("code") // 接下来,我们通过 clientID,clientSecret,code 获取授权密钥 // 前者是我们在注册时得到的,后者是用户确认后,重定向到该路由,从中获取到的。 reqURL := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", clientID, clientSecret, code) req, err := http.NewRequest(http.MethodPost, reqURL, nil) if err != nil { log.Printf("could not create HTTP request: %v", err) w.WriteHeader(http.StatusBadRequest) } // 设置我们期待返回的格式为json req.Header.Set("accept", "application/json") // 发送http请求 res, err := httpClient.Do(req) if err != nil { log.Printf("could not send HTTP request: %v", err) w.WriteHeader(http.StatusInternalServerError) } defer res.Body.Close() // 解析 var t OAuthAccessResponse if err := json.NewDecoder(res.Body).Decode(&t); err != nil { log.Printf("could not parse JSON response: %v", err) w.WriteHeader(http.StatusBadRequest) } // 最后获取到access_token后,我们重定向到欢迎页面,也就是表示用户登录成功,同属获取一些用户的基本展示信息 w.Header().Set("Location", "/welcome.html?access_token="+t.AccessToken) w.WriteHeader(http.StatusFound) }
欢迎页面
上面我们拿到了访问令牌,同时,我们重定向到了欢迎页面。接下来,我们就在欢迎页中获取用户的GitHub
名称作为展示。
添加public/welcome.html
页面。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Hello</title> </head> <body> </body> <script> // 获取access_token const query = window.location.search.substring(1) const token = query.split('access_token=')[1] // 访问资源服务器地址,获取相关资源 fetch('https://api.github.com/user', { headers: { // 将token放在Header中 Authorization: 'token ' + token } }) // 解析返回的JSON .then(res => res.json()) .then(res => { // 这里我们能得到很多信息 // 具体看这里 https://developer.github.com/v3/users/#get-the-authenticated-user // 这里我们就只展示一下用户名了 const nameNode = document.createTextNode(`Welcome, ${res.name}`) document.body.appendChild(nameNode) }) </script>
到这里,我们再次运行程序,点击Login with github
,同意后我们可以看到类似Welcome, Razeen
的信息,此时我们已经完成整个OAuth 2.0
的流程了。
获得授权之后,我们能访问的
API
不仅仅有这些,更多的请看这里。
关于安全
- 这里我们将访问令牌直接放在了URI中,这么做其实是不安全的。更好的做法是我们创建一个会话
session
,将cooike
发送给用户即可。 - 在前面
OAuth 2.0
的解释中,我们知道在请求权限的过程中,我们可以加上state
字段,如
https://github.com/login/oauth/authorize? client_id=89ac6f58f15f658d8dd5 &redirect_uri=http://localhost:8080/oauth/redirect &state=xxxxx
- 而服务器会原封不动的返回该state。这样我们可以将该字段设置一些随机值,如果资源服务器返回的
state
值与我们设置的不同,我们认为该请求不是来自正确的资源服务器,应该拒绝。