OAuth 就像一个 BOSS 常用的东西 从另一个主机登录 从浏览器扩展登录 从另一种语言登录 结论

2025-06-07

像老板一样进行 OAuth

常见事物

从另一台主机登录

从浏览器扩展登录

从其他语言登录

结论

在我之前的文章中,我谈到了格兰特

NodeJS 的 OAuth 中间件,也恰好是一个完全透明的 OAuth 代理。

这次我们将探讨一些现实世界的例子:

  1. 从用 JavaScript 编写的服务器应用程序登录。
  2. 从 GitHub Pages 上托管的浏览器应用程序登录。
  3. 从浏览器扩展登录。
  4. 从用另一种编程语言编写的服务器应用程序登录。

常见事物

假设我们有一个托管在 GitHub 上的 Web 应用awesome.com,并且该应用利用GitHub API来管理用户的仓库。我们的网站上还有一个 Web 表单,允许用户选择他们愿意授予我们应用的权限:

<form action="/connect/github" method="POST">
  <p>Grant read/write access to:</p>
  <label>
    <input type="radio" group="scope" name="scope" value="repo" />
    public and private repositories</label>
  <label>
    <input type="radio" group="scope" name="scope" value="public_repo" />
    public repositories only</label>
  <button>Login</button>
</form>

此表单将把POST所选的 OAuth路由scopeGrant所操作的/connect/github路线。

接下来我们需要一个Grant服务器来为我们处理 OAuth 流程:

var express = require('express')
var session = require('express-session')
var parser = require('body-parser')
var grant = require('grant-express')

express()
  .use(session({secret: 'dev.to'}))
  .use(parser.urlencoded()) // only needed for POST requests
  .use(grant(require('./config.json')))
  .use('/login', (req, res) => res.end(`the above HTML form`))
  .use('/hello', (req, res) => {
    var {access_token} = req.session.grant.response
    console.log(access_token)
    res.end('nice!')
  })
  .listen(3000)

采用以下配置:

{
  "defaults": {
    "origin": "https://awesome.com", "state": true, "transport": "session"
  },
  "github": {
    "key": "...", "secret": "...", "dynamic": ["scope"], "callback": "/hello"
  }
}

我们允许将 OAuthscope设置dynamic为 GitHub 的盟友。我们还将使用会话transport在最终路由中传递 OAuth 流程的结果callback

最后,我们必须在 GitHub 上创建一个真正的OAuth 应用key,并将其和复制粘贴secret到上面的配置中。我们还必须将其授权回调 URL设置为,这是Granthttps://awesome.com/connect/github/callback保留的第二条路由

这将允许我们选择一个范围并通过导航到以下位置登录 GitHubhttps://awesome.com/login

从另一台主机登录

现在想象一下,我们在GitHub Pages托管了另一个应用程序https://simov.github.io/stars/,该应用程序允许用户探索托管在 GitHub 上的给定存储库收到的星星的统计数据和历史记录。

我们的应用将仅访问公共数据。遗憾的是,GitHub 对其 API 的默认速率限制为每小时 60 个 HTTP 请求。但是,如果请求与访问令牌一起发送,速率限制将提升至每小时最多 5000 个 HTTP 请求。

因此,我们需要再次登录,我们已经有一个Grant服务器启动并运行awesome.com,所以为什么不重复使用它呢:

{
  "defaults": {
    "origin": "https://awesome.com", "state": true, "transport": "session"
  },
  "github": {
    "key": "...", "secret": "...", "dynamic": ["scope"], "callback": "/hello",
    "overrides": {
      "stars": {
        "key": "...", "secret": "...", "dynamic": ["callback"], "transport": "querystring"
      }
    }
  }
}

我们希望为 GitHub 创建一个名为 的子配置stars。它将是一个不同的OAuth 应用,请注意keysecret

我们还想设置最终callbackURL dynamic,但不设置scope其上方的允许。我们将在不设置任何显式范围的情况下进行登录,对于 GitHub 来说,这意味着仅获得对公共数据的读取权限。

最后,我们重写了transport从 继承的defaults。我们需要将响应数据编码为查询字符串,并放在完全限定的绝对callbackURL 中,指向托管在GitHub Pages上的浏览​​器应用。

然后我们必须导航到连接路由来登录:

// store the current URL
localStorage.setItem('redirect', location.href)
// set callback URL dynamically - https://simov.github.io/stars/
var callback = encodeURIComponent(location.origin + location.pathname)
// navigate to the connect route
location.href = `https://awesome.com/connect/github/stars?callback=${callback}`

callback如果我们将来想在不同的域上托管我们的应用程序,那么动态设置我们的最终 URL 会很方便。

登录后,用户将被重定向回我们在 GitHub 上托管的浏览器应用程序:

https://simov.github.io/stars/?access_token=...

现在是从查询字符串中提取access_token并存储以供将来使用的时候了:

var qs = new URLSearchParams(location.search)
localStorage.setItem('token', qs.get('access_token'))

在开始使用我们的 OAuth 应用登录之前,将用户重定向回原来的位置是一个很好的最后一步:

location.href = localStorage.getItem('redirect') // go back to the last URL
localStorage.removeItem('redirect')

这就是我之前谈论的应用程序。

从浏览器扩展登录

接下来我们创建一个浏览器扩展,它通过添加一个很酷的小按钮来增强 GitHub 的 UI。点击按钮后,它会汇总一些有关我们当前正在浏览的仓库的有用信息:

{
  "manifest_version": 2,
  "name": "Cool Little Button",
  "version": "1.0.0",
  "background" : {"scripts": ["background.js"]},
  "content_scripts": [{"matches": ["https://github.com/*"], "js": ["content.js"]}],
  "permissions": ["storage"]
}

然而,此扩展依赖于从GitHub API获取的数据,默认情况下,这将再次限制我们每小时 60 个 HTTP 请求。因此,如果能让用户直接从我们的扩展程序中快速轻松地登录,并将速率限制提升到每小时最多 5000 个 HTTP 请求,那就太好了:

{
  "defaults": {
    "origin": "https://awesome.com", "state": true, "transport": "session"
  },
  "github": {
    "key": "...", "secret": "...", "dynamic": ["scope"], "callback": "/hello",
    "overrides": {
      "stars": {
        "key": "...", "secret": "...", "dynamic": ["callback"], "transport": "querystring"
      },
      "extension": {
        "dynamic": false, "transport": "querystring",
        "callback": "https://github.com/extension/callback"
      }
    }
  }
}

这次我们将重用从 GitHub 配置根级别(以及key)继承的 OAuth 应用secret。我们也不希望用户dynamic单独设置任何配置选项。

然后我们可以从脚本中打开一个新选项卡background.js并让我们的用户登录:

chrome.tabs.create({url: 'https://awesome.com/connect/github/extension')})

我们将把用户重定向回 GitHub 上一个不存在的页面。在这种情况下,GitHub 将返回一个通用的 HTML 页面来响应 404 Not Found 状态码,我们的访问令牌将被编码到查询字符串中:

https://github.com/extension/callback?access_token=...

因此,我们需要做的就是提取并存储它,这次将以下代码放入content.js脚本中:

var qs = new URLSearchParams(location.search)
chrome.storage.sync.set({token: qs.get('access_token')})

从其他语言登录

Grant不会限制我们在服务器上只能使用 JavaScript 和 NodeJS。Grant 能够通过 HTTP 动态配置,并将结果返回到我们想要的位置,让我们能够使用任何其他编程语言访问180 多个登录提供程序

这次我们将子配置命名为proxy,允许dynamic配置 Grant 中可用的每个选项:

{
  "defaults": {
    "origin": "https://awesome.com", "state": true, "transport": "session"
  },
  "github": {
    "key": "...", "secret": "...", "dynamic": ["scope"], "callback": "/hello",
    "overrides": {
      "stars": {
        "key": "...", "secret": "...", "dynamic": ["callback"], "transport": "querystring"
      },
      "extension": {
        "dynamic": false, "transport": "querystring",
        "callback": "https://github.com/extension/callback"
      },
      "proxy": {
        "dynamic": true
      }
    }
  }
}

为了这个例子的目的,我将使用Go,我只需要选择一种,但以下内容适用于任何其他语言:

package main
import (
  "fmt"
  "net/url"
  "net/http"
)
func main() {
  http.HandleFunc("/login", func (w http.ResponseWriter, r *http.Request) {
    qs := url.Values{}
    qs.Add("key", "...") // yes
    qs.Add("secret", "...") // we're passing an OAuth app dynamically!
    qs.Add("scope", "repo user gist")
    qs.Add("transport", "querystring")
    qs.Add("callback", "http://localhost:3000/hello")
    http.Redirect(w, r, "https://awesome.com/connect/github/proxy?" + qs.Encode(), 301)
  })
  http.HandleFunc("/hello", func (w http.ResponseWriter, r *http.Request) {
    qs, _ := url.ParseQuery(r.URL.RawQuery)
    fmt.Println(qs["access_token"][0])
    w.Write([]byte("nice!"))
  })
  http.ListenAndServe(":3000", nil)
}

现在我们需要做的就是导航到http://localhost:3000/login

结论

好吧,我可以用 5 行 JSON 配置来概括整篇文章。

然而,它的目的在于演示服务器端登录如何与各种类型的应用程序连接,以及Grant如何适应更广泛的情况。

希望这可以为想要利用服务器端登录的前端开发人员以及想要快速访问大量提供商的后端开发人员提供方便的指南。

这是所有示例的源代码。

祝您编码愉快!

文章来源:https://dev.to/simov/oauth-like-a-boss-2m3b
PREV
作为 JavaScript 开发人员必须知道的 5 个对象方法 2. Object.create() 3. Object.entries() 4. Object.freeze() 5. Object.is()
NEXT
如何在 2023 年成为一名云工程师