阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

给 Nginx 增加 OAuth 支持(nginx-lua)

180次阅读
没有评论

共计 13587 个字符,预计需要花费 34 分钟才能阅读完成。

**白话不表 ** 我 们使用 Nginx 的 Lua 中间件建立了 OAuth2 认证和授权层。如果你也有此打算,阅读下面的文档,实现自动化并获得收益。

SeatGeek 在过去几年中取得了发展,我们已经积累了不少针对各种任务的不同管理接口。我们通常为新的展示需求创建新模块,比如我们自己的博客、图表等。我们还定期开发内部工具来处理诸如部署、可视化操作及事件处理等事务。在处理这些事务中,我们使用了几个不同的接口来认证:

  • Github/Google Oauth

  • 我们 SeatGeek 内部的用户系统

  • 基本认证

  • 硬编码登录

显然,实际应用中很不规范。多个认证系统使得难以对用于访问级别和通用许可的各种数据库进行抽象。

单系统认证

我们也做了一些关于如何设置将解决我们问题的研究。这促使了 Odin 的出现,它在验证谷歌应用的用户方面工作的很好。不幸的是它需要使用 Apache,而我们已和 Nginx 结为连理并把它作为我们的后端应用的前端。

幸运的是,我看了 mixlr 的博客并引用了他们 Lua 在 Nginx 上的应用:

  • 修改响应头

  • 重写内部请求

  • 选择性地基于 IP 拒绝主机访问

最后一条看起来很有趣。它开启了软件包管理的地狱之旅。

构建支持 Lua 的 Nginx

Lua for Nginx 没有被包含在 Nginx 的核心中,我们经常要为 OSX 构建 Nginx 用于开发测试,为 Linux 构建用于部署。

为 OSX 定制 Nginx

对于 OSX 系统,我推荐使用 Homebrew 进行包管理。它初始的 Nginx 安装包启用的模块不多,这有非常好的理由:

关键在于 NGINX 有着如此之多的选项,如果把它们都加入初始包那一定是疯了,如果我们只把其中一些加入其中就会迫使我们把所有都加入,这会让我们疯掉的。
Charlie Sharpsteen, @sharpie

所以我们需要自己构建。合理地构建 Nginx 可以方便我们以后继续扩展。幸运的是,使用 Homebrew 进行包管理十分方便快捷。

我们首先需要一个工作空间:

cd ~
mkdir -p src
cd src

 

之后,我们需要找到初始安装信息包。你可以通过下面任何一种方式得到它:

  • 找到 HOMEBREW_PREFIX 目录,通常在 /usr/local 下,在其中找到 nginx.rb 文件

  • 从下列地址取得 https://raw.github.com/mxcl/homebrew/master/Library/Formula/nginx.rb

  • 使用如下命令 brew cat nginx > nginx.rb

此时如果我们执行 brew install ./nginx.rb 命令,它会依据其中的信息安装 Nginx。既然现在我们要完全定制 Nginx,我们要重命名信息包,这样之后通过 brew update 命令进行更新的时候就不会覆盖我们自定义的了:

mv nginx.rb nginx-custom.rb
cat nginx-custom.rb | sed 's/class Nginx/class NginxCustom/' >> tmp
rm nginx-custom.rb
mv tmp nginx-custom.rb

 

我们现在可以将我们需要的模块加入安装信息包中并开始编译了。这很简单,我们只要将所有我们需要的模块以参数形式传给 brew install 命令,代码如下:

# Collects arguments from ARGV
def collect_modules regex=nil
    ARGV.select {|arg| arg.match(regex) != nil }.collect {|arg| arg.gsub(regex, '') }
end

# Get nginx modules that are not compiled in by default specified in ARGV
def nginx_modules; collect_modules(/^--include-module-/); end

# Get nginx modules that are available on github specified in ARGV
def add_from_github; collect_modules(/^--add-github-module=/); end

# Get nginx modules from mdounin's hg repository specified in ARGV
def add_from_mdounin; collect_modules(/^--add-mdounin-module=/); end

# Retrieve a repository from github
def fetch_from_github name
    name, repository = name.split('/')
    raise "You must specify a repository name for github modules" if repository.nil?

    puts "- adding #{repository} from github..."
    `git clone -q git://github.com/#{name}/#{repository} modules/#{name}/#{repository}`
    path = Dir.pwd + '/modules/' + name + '/' + repository
end

# Retrieve a tar of a package from mdounin
def fetch_from_mdounin name
    name, hash = name.split('#')
    raise "You must specify a commit sha for mdounin modules" if hash.nil?

    puts "- adding #{name} from mdounin..."
    `mkdir -p modules/mdounin && cd $_ ; curl -s -O http://mdounin.ru/hg/#{name}/archive/#{hash}.tar.gz; tar -zxf #{hash}.tar.gz`
    path = Dir.pwd + '/modules/mdounin/' + name + '-' + hash
end

 

上面这个辅助模块可以让我们指定想要的模块并检索模块的地址。现在,我们需要修改 nginx-custom.rb 文件,使之包含这些模块的名字并在包中检索它们,在 58 行附近:

nginx_modules.each {|name| args << "--with-#{name}"; puts "- adding #{name} module" }
add_from_github.each {|name| args <<  "--add-module=#{fetch_from_github(name)}" }
add_from_mdounin.each {|name| args <<  "--add-module=#{fetch_from_mdounin(name)}" }

 

现在我们可以编译我们重新定制的 nginx 了:

brew install ./nginx-custom.rb \
    --add-github-module=agentzh/chunkin-nginx-module \
    --include-module-http_gzip_static_module \
    --add-mdounin-module=ngx_http_auth_request_module#a29d74804ff1

 

你可以方便地在 seatgeek/homebrew-formulae 找到以上信息包。

Nginx 的详细介绍:请点这里
Nginx 的下载地址:请点这里

推荐阅读

 

Nginx 实现反向代理和负载均衡的配置及优化 http://www.linuxidc.com/Linux/2013-11/92909.htm

 

Nginx 做负载均衡报:nginx: [emerg] could not build the types_hash http://www.linuxidc.com/Linux/2013-10/92063.htm

 

Nginx 负载均衡模块 ngx_http_upstream_module 详述 http://www.linuxidc.com/Linux/2013-10/91907.htm

 

Nginx+Firebug 让浏览器告诉你负载均衡将请求分到了哪台服务器 http://www.linuxidc.com/Linux/2013-10/91824.htm

 

Ubuntu 安装 Nginx php5-fpm MySQL(LNMP 环境搭建) http://www.linuxidc.com/Linux/2012-10/72458.htm

 

为 Debian 定制 Nginx

我们通常都会部署到 Debian 的发行版 - 通常是 Ubuntu 上作为我们的产品服务器。如果是这样,那将会非常简单,运行 dpkg -i nginx-custom 安装我们的定制包。这步骤如此简单你一运行它就完成了。

一些在搜索定制 debian/ubuntu 包时的笔记:

  • 你可以通过 apt-get source PACKAGE_NAME 来获取 debian 安装包。

  • Debian 安装包受控于一个 rules 文件,你需要 sed-fu 来操作它。

  • 你可以通过编辑 control 文件来更新 deb 包的依赖。注意这里指定了一些元依赖 (meta-dependency) 你不要去删除它,但是这些很容易分辨出来。

  • 新的发布必须要在 changelog 里注明,否则包有可能不会被升级因为它可能已经被安装过了。你需要在表单里使用 +tag_name 来指明哪些是你自己在 baseline 上新加的改动。我会额外加上一个数字 - 从 0 开始 - 指示出包的发布编号。

  • 大多数的改动可以以某种方式自动更改,但是似乎没有一个简单的命令行工具可以创建定制的发布包。这也正是我们感兴趣的地方,如果你知道什么的话,请给我们给我们提供一些链接,工具或方法。

在运行这个伟大过程的同时,我构建了一个小的批处理脚本来自动化这个过程的主要步骤,你可以在 gist on github 上找到它。

在我意识到这个过程可以被脚本化之前仅仅花费了 90 个 nginx 包的构建时间。

全部 OAuth

现在可以测试并部署嵌入 Nginx 的 Lua 脚本了,让我们开始 Lua 编程。

nginx-lua 模块提供了一些辅助功能和变量来访问 Nginx 的绝大多数功能,显然我们可以通过 access_by_lua 中该模块提供的指令来强制打开 OAuth 认证。

当使用 *_by_lua_file 指令后,必须重载 nginx 来使其起作用。

我为 SeatGeek 用 NodeJS 创建了一个简单的 OAuth2 提供者类。这部分内容很简单,你也很容易获得你是通用语言的响应版本。

接下来,我们的 OAuth API 使用 JSON 来处理令牌(token)、访问级别(access level)和重新认证响应(re-authentication responses)。所以我们需要安装 lua-cjson 模块。

# install lua-cjson
if [! -d lua-cjson-2.1.0]; then
    tar zxf lua-cjson-2.1.0.tar.gz
fi
cd lua-cjson-2.1.0
sed 's/i686/x86_64/' /usr/share/lua/5.1/luarocks/config.lua > /usr/share/lua/5.1/luarocks/config.lua-tmp
rm /usr/share/lua/5.1/luarocks/config.lua
mv /usr/share/lua/5.1/luarocks/config.lua-tmp /usr/share/lua/5.1/luarocks/config.lua
luarocks make

 

我的 OAuth 提供者类使用了 query-string 来发送认证的错误信息,我们也需要在我们的 Lua 脚本中为其提供支持:

local args = ngx.req.get_uri_args()
if args.error and args.error == "access_denied" then
    ngx.status = ngx.HTTP_UNAUTHORIZED
    ngx.say("{\"status\": 401, \"message\": \""..args.error_description.."\"}")
    return ngx.exit(ngx.HTTP_OK)
end

 

现在我们解决了基本的错误情况,我们要为访问令牌设置 cookie。在我的例子中,cookie 会在访问令牌过期前过期,所以我可以利用 cookie 来刷新访问令牌。

local access_token = ngx.var.cookie_SGAccessToken
if access_token then
    ngx.header["Set-Cookie"] = "SGAccessToken="..access_token.."; path=/;Max-Age=3000"
end

 

现在,我们解决了错误响应的 api,并储存了 access_token 供后续访问。我们现在需要确保 OAuth 认证过程正确启动。下面,我们想要:

  • 如果没有 access_token 已经或将要存储,开启 OAuth 认证

  • 如果 query string 的参数中有 OAuth 访问代码(access code),使用 OAuth API 检索用户的 access_token

  • 拒绝使用非法访问代码用户的请求

Lua 的详细介绍:请点这里
Lua 的下载地址:请点这里

推荐阅读

Lua 语言 15 分钟快速入门 http://www.linuxidc.com/Linux/2013-06/86582.htm

Lua 程序设计(第 2 版)中文 PDF http://www.linuxidc.com/Linux/2013-03/81833.htm

Lua 程序设计(第二版)阅读笔记 http://www.linuxidc.com/Linux/2013-03/81834.htm

NetBSD 将支持用 Lua 脚本开发内核组件 http://www.linuxidc.com/Linux/2013-02/79527.htm

CentOS 编译安装 Lua LuaSocket http://www.linuxidc.com/Linux/2011-08/41105.htm

**白话不表 ** 我 们使用 Nginx 的 Lua 中间件建立了 OAuth2 认证和授权层。如果你也有此打算,阅读下面的文档,实现自动化并获得收益。

SeatGeek 在过去几年中取得了发展,我们已经积累了不少针对各种任务的不同管理接口。我们通常为新的展示需求创建新模块,比如我们自己的博客、图表等。我们还定期开发内部工具来处理诸如部署、可视化操作及事件处理等事务。在处理这些事务中,我们使用了几个不同的接口来认证:

  • Github/Google Oauth

  • 我们 SeatGeek 内部的用户系统

  • 基本认证

  • 硬编码登录

显然,实际应用中很不规范。多个认证系统使得难以对用于访问级别和通用许可的各种数据库进行抽象。

单系统认证

我们也做了一些关于如何设置将解决我们问题的研究。这促使了 Odin 的出现,它在验证谷歌应用的用户方面工作的很好。不幸的是它需要使用 Apache,而我们已和 Nginx 结为连理并把它作为我们的后端应用的前端。

幸运的是,我看了 mixlr 的博客并引用了他们 Lua 在 Nginx 上的应用:

  • 修改响应头

  • 重写内部请求

  • 选择性地基于 IP 拒绝主机访问

最后一条看起来很有趣。它开启了软件包管理的地狱之旅。

构建支持 Lua 的 Nginx

Lua for Nginx 没有被包含在 Nginx 的核心中,我们经常要为 OSX 构建 Nginx 用于开发测试,为 Linux 构建用于部署。

为 OSX 定制 Nginx

对于 OSX 系统,我推荐使用 Homebrew 进行包管理。它初始的 Nginx 安装包启用的模块不多,这有非常好的理由:

关键在于 NGINX 有着如此之多的选项,如果把它们都加入初始包那一定是疯了,如果我们只把其中一些加入其中就会迫使我们把所有都加入,这会让我们疯掉的。
Charlie Sharpsteen, @sharpie

所以我们需要自己构建。合理地构建 Nginx 可以方便我们以后继续扩展。幸运的是,使用 Homebrew 进行包管理十分方便快捷。

我们首先需要一个工作空间:

cd ~
mkdir -p src
cd src

 

之后,我们需要找到初始安装信息包。你可以通过下面任何一种方式得到它:

  • 找到 HOMEBREW_PREFIX 目录,通常在 /usr/local 下,在其中找到 nginx.rb 文件

  • 从下列地址取得 https://raw.github.com/mxcl/homebrew/master/Library/Formula/nginx.rb

  • 使用如下命令 brew cat nginx > nginx.rb

此时如果我们执行 brew install ./nginx.rb 命令,它会依据其中的信息安装 Nginx。既然现在我们要完全定制 Nginx,我们要重命名信息包,这样之后通过 brew update 命令进行更新的时候就不会覆盖我们自定义的了:

mv nginx.rb nginx-custom.rb
cat nginx-custom.rb | sed 's/class Nginx/class NginxCustom/' >> tmp
rm nginx-custom.rb
mv tmp nginx-custom.rb

 

我们现在可以将我们需要的模块加入安装信息包中并开始编译了。这很简单,我们只要将所有我们需要的模块以参数形式传给 brew install 命令,代码如下:

# Collects arguments from ARGV
def collect_modules regex=nil
    ARGV.select {|arg| arg.match(regex) != nil }.collect {|arg| arg.gsub(regex, '') }
end

# Get nginx modules that are not compiled in by default specified in ARGV
def nginx_modules; collect_modules(/^--include-module-/); end

# Get nginx modules that are available on github specified in ARGV
def add_from_github; collect_modules(/^--add-github-module=/); end

# Get nginx modules from mdounin's hg repository specified in ARGV
def add_from_mdounin; collect_modules(/^--add-mdounin-module=/); end

# Retrieve a repository from github
def fetch_from_github name
    name, repository = name.split('/')
    raise "You must specify a repository name for github modules" if repository.nil?

    puts "- adding #{repository} from github..."
    `git clone -q git://github.com/#{name}/#{repository} modules/#{name}/#{repository}`
    path = Dir.pwd + '/modules/' + name + '/' + repository
end

# Retrieve a tar of a package from mdounin
def fetch_from_mdounin name
    name, hash = name.split('#')
    raise "You must specify a commit sha for mdounin modules" if hash.nil?

    puts "- adding #{name} from mdounin..."
    `mkdir -p modules/mdounin && cd $_ ; curl -s -O http://mdounin.ru/hg/#{name}/archive/#{hash}.tar.gz; tar -zxf #{hash}.tar.gz`
    path = Dir.pwd + '/modules/mdounin/' + name + '-' + hash
end

 

上面这个辅助模块可以让我们指定想要的模块并检索模块的地址。现在,我们需要修改 nginx-custom.rb 文件,使之包含这些模块的名字并在包中检索它们,在 58 行附近:

nginx_modules.each {|name| args << "--with-#{name}"; puts "- adding #{name} module" }
add_from_github.each {|name| args <<  "--add-module=#{fetch_from_github(name)}" }
add_from_mdounin.each {|name| args <<  "--add-module=#{fetch_from_mdounin(name)}" }

 

现在我们可以编译我们重新定制的 nginx 了:

brew install ./nginx-custom.rb \
    --add-github-module=agentzh/chunkin-nginx-module \
    --include-module-http_gzip_static_module \
    --add-mdounin-module=ngx_http_auth_request_module#a29d74804ff1

 

你可以方便地在 seatgeek/homebrew-formulae 找到以上信息包。

Nginx 的详细介绍:请点这里
Nginx 的下载地址:请点这里

推荐阅读

 

Nginx 实现反向代理和负载均衡的配置及优化 http://www.linuxidc.com/Linux/2013-11/92909.htm

 

Nginx 做负载均衡报:nginx: [emerg] could not build the types_hash http://www.linuxidc.com/Linux/2013-10/92063.htm

 

Nginx 负载均衡模块 ngx_http_upstream_module 详述 http://www.linuxidc.com/Linux/2013-10/91907.htm

 

Nginx+Firebug 让浏览器告诉你负载均衡将请求分到了哪台服务器 http://www.linuxidc.com/Linux/2013-10/91824.htm

 

Ubuntu 安装 Nginx php5-fpm MySQL(LNMP 环境搭建) http://www.linuxidc.com/Linux/2012-10/72458.htm

 

阅读 nginx-Lua 函数和变量的相关文档可以解决一些问题,或许还能告诉你访问特定请求 / 响应信息的各种方法。

此时,我们需要从我们的 api 接口获取一个 TOKEN。nginx-lua 提供了 ngx.location.capture 方法,支持发起一个内部请求到 redis,并接收响应。这意味着,我们不能直接调用类似于 http://seatgeek.com/ncaa-football-tickets,但我们可以用 proxy_pass 把这种外部链接包装成内部请求。

我们通常约定给这样的内部请求前面加一个_(下划线), 用来阻止外部直接访问。

-- 第一步,从 api 获取获取 token
if not access_token or args.code then
    if args.code then
        -- internal-oauth:1337/access_token
        local res = ngx.location.capture("/_access_token?client_id="..app_id.."&client_secret="..app_secret.."&code="..args.code)

        -- 终止所有非法请求
        if res.status ~= 200 then
            ngx.status = res.status
            ngx.say(res.body)
            ngx.exit(ngx.HTTP_OK)
        end

        -- 解码 token
        local text = res.body
        local json = cjson.decode(text)
        access_token = json.access_token
    end

    -- cookie 和 proxy_pass token 请求失败
    if not access_token then
        -- 跟踪用户访问,用于透明的重定向
        ngx.header["Set-Cookie"] = "SGRedirectBack="..nginx_uri.."; path=/;Max-Age=120"

        -- 重定向到 /oauth , 获取权限
        return ngx.redirect("internal-oauth:1337/oauth?client_id="..app_id.."&scope=all")
    end
end

 

此时在 Lua 脚本中,应该已经有了一个可用的 access_token。我们可以用来获取任何请求需要的用户信息。在本文中,返回 401 表示没有权限,403 表示 token 过期,并且授权信息用简单数字打包成 json 响应。

-- 确保用户有访问 web 应用的权限
-- internal-oauth:1337/accessible
local res = ngx.location.capture("/_user", {args = { access_token = access_token} } )
if res.status ~= 200 then
    -- 删除损坏的 token
    ngx.header["Set-Cookie"] = "SGAccessToken=deleted; path=/; Expires=Thu, 01-Jan-1970 00:00:01 GMT"

    -- 如果 token 损坏,重定向 403 forbidden 到 oauth
    if res.status == 403 then
        return ngx.redirect("https://seatgeek.com/oauth?client_id="..app_id.."&scope=all")
    end

    -- 没有权限
    ngx.status = res.status
    ngx.say("{"status": 503,"message":"Error accessing api/me for credentials"}")
    return ngx.exit(ngx.HTTP_OK)
end

现在,我们已经验证了用户确实是经过身份验证的并且具有某个级别的访问权限,我们可以检查他们的访问级别,看看是否同我们所定义的任何当前端点的访问级别有冲突。我个人在这一步删除了 SGAccessToken,以便用户拥有使用不同的用户身份登录的能力,但这一做法用不用由你决定。

local json = cjson.decode(res.body)
-- Ensure we have the minimum for access_level to this resource
if json.access_level < 255 then
    -- Expire their stored token
    ngx.header["Set-Cookie"] = "SGAccessToken=deleted; path=/; Expires=Thu, 01-Jan-1970 00:00:01 GMT"

    -- Disallow access
    ngx.status = ngx.HTTP_UNAUTHORIZED
    ngx.say("{\"status\": 403, \"message\": \"USER_ID"..json.user_id.." has no access to this resource\"}")
    return ngx.exit(ngx.HTTP_OK)
end

-- Store the access_token within a cookie
ngx.header["Set-Cookie"] = "SGAccessToken="..access_token.."; path=/;Max-Age=3000"

-- Support redirection back to your request if necessary
local redirect_back = ngx.var.cookie_SGRedirectBack
if redirect_back then
    ngx.header["Set-Cookie"] = "SGRedirectBack=deleted; path=/; Expires=Thu, 01-Jan-1970 00:00:01 GMT"
    return ngx.redirect(redirect_back)
end

 

现在我们只需要通过一些请求头信息告知我们当前的应用谁登录了就行了。您可以重用 REMOTE_USER,如果你有需求的话,就可以用这个取代基本的身份验证,而除此之外的任何事情都是公平的游戏。

-- Set some headers for use within the protected endpoint
ngx.req.set_header("X-USER-ACCESS-LEVEL", json.access_level)
ngx.req.set_header("X-USER-EMAIL", json.email)

 

我现在就可以像任何其它的站点那样在我的应用程序中访问这些 http 头了,而不是用数百行代码和大量的时间来重新实现身份验证。

Nginx 和 Lua, 放在树结构里面

在这一点上,我们应该有一个可以用来阻挡 / 拒绝访问的 LUA 脚本。我们可以将这个脚本放到磁盘上的一个文件中,然后使用 access_by_lua_file 配置来将它应用在我们的 nginx 站点中。在 SeatGeek 中,我们使用 Chief 来模板化输出配置文件,虽然你可以使用 Puppet,Fabric,或者其它任何你喜欢的工具。

下面是你可以用来使所有东西都运行起来的最简单的 nginx 的网站。你也可能会想要检查下 access.lua – 在这里 – 它是上面的 lua 脚本编译后的文件。

# The app we are proxying to
upstream production-app {server localhost:8080;}

# The internal oauth provider
upstream internal-oauth {server localhost:1337;}

server {
  listen       80;
  server_name  private.example.com;
  root         /apps;
  charset      utf-8;

  # This will run for everything but subrequests
  access_by_lua_file "/etc/nginx/access.lua";

  # Used in a subrequest
  location /_access_token {proxy_pass http://internal-oauth/oauth/access_token;}
  location /_user {proxy_pass http://internal-oauth/user;}

  location / {
    proxy_set_header  X-Real-IP  $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  Host $http_host;
    proxy_redirect    off;
    proxy_max_temp_file_size 0;

    if (!-f $request_filename) {
      proxy_pass http://production-app;
      break;
    }
  }

}

进一步思考

虽然此设置运行的比较好,但是我想指出一些缺点:

  • 上面的代码是我们 access_by_lua 脚本的简化。我们也处理保存 POST 提交的请求,JS 加入到到页面更新会话自动处理的令牌更新等,你可能不需要这些功能,而事实上,我不认为我需要它们,直到我们开始了我们在内部系统进行系统测试。

  • 我们有一些结点,可以通过一定的后台任务基本认证。这些被修改,数据是从一个外部存储中检索,如 S3。注意,这并不总是可能的,所以使用的可能不是你想要的答案。

  • Oauth2 只是我选择的标准。在理论上,你可以使用 facebook 授权来实现类似的结果。你也可以将这种方法限速,或存储在数据库中的不同的访问级别如在你的 Lua 脚本方便操作和检索使用。如果你真的很无聊,你可以重新实现基本认证在 Lua,这只需要你。

  • 有没有测试控制系统等。测试者会害怕当他们意识到这将是一段时间的集成测试。你可以重新运行上面的嘲笑为全球范围内注入变量以及执行脚本,但它不是理想的设置。

  • 你还需要修改应用程序识别你的新的访问标头。内部工具将是最简单的,但你可能需要为供应商软件作出一定的让步。

    TL;DR 链接

一些博客中的讲解及研究实例

链接

  • SeatGeek Homebrew Formulae with customizable nginx

  • nginx_release.sh for building nginx debs

  • access.lua and nginx-site

另一些讲解阅读

  • HttpLuaModule

  • proxy_pass, like mod_proxy, but for nginx

  • Lua usage at Mixlr

  • OAuth for Apache

  • Homebrew OS X Package Manager

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-20发表,共计13587字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中