使用 Satis 搭建属于自己的私有仓库(带认证)

而在某些情况下,我们可能希望将自己的 PHP 包保存在一个私有仓库中,以便更好地管理、分享和维护项目的依赖项。

Satis 是一款强大的工具,它让你能够轻松地搭建属于自己的 Composer 仓库,无论是为了内部使用、团队协作,还是出于其他安全和隐私的考虑。本文将向您介绍如何使用 Satis,一步步地构建一个私有仓库。

社区好像也没有关于如何出售自己的私有仓库的文章。文章最后将会介绍如何将私有仓库接入到项目的认证系统,并授权给用户使用,这是一个很好的方式,当然这种方法不会对源码进行保护。但也比源码直接泄露出去强很多了。

创建项目

首先创建一个空的 satis 项目,主要按照相关依赖,里面只包含一个 vendor 目录

mkdir  private
cd  privite
composer  require  "composer/satis:*"  --ignore-platform-reqs  --with-all-dependencies

依赖安装完成之后,查看下目录结构。大概就是以下几个文件

|---  composer.json
|---  composer.lock
|---  vendor/

配置 satis.json


{

 "name": "private/package", // 随便起个名称
 "homepage": "http://satic.xxxxx.com/", // 私有仓库域名
 "output-dir": "dist", // 输出目录
 "repositories": [
 // 配置包的 vcs 地址
    {
         "type": "vcs",
         "url": "https://git.xxxxxx.com/satis/package"  // 这里设置私有仓库地址,记得
    }
  ],
 "archive": {
     "directory": "dist", // 包的存储目录
     "skip-dev": false
  },
 "require-all": true
}

配置好了 satis.json 之后,就可以使用下面的命令

php  vendor/bin/satis  build

执行之后,你将会看到以下输出。

scanning  packages
...

这个命令会扫描配置版本控制系统的地址,并且把所有的对应仓库版本打包下载到 dist 目录。

如果你执行到了这一步,那么你应该会看到 dist 目录下面的 dist/

配置 nginx 来访问项目。假设这里你配置的域名http://satic.xxxxx.com/,步骤这里就省略了。在配置完成之后。访问 http://satic.xxxxx.com/index.html的时候,会看到这样的页面,说明你已经成功配置了。

使用 Satis 搭建属于自己的私有仓库

虽然已经完成了私有仓库的配置,也看到了私有包被下载下来了,但这里还需要在 composer.json 中配置一下,才可以通过 composer 来下载。打开项目的 composer.json,加上下面的配置

"repositories": [
      {
         "type": "composer",
         "url": "http://satic.xxxxx.com/"  // 设置仓库,就是 satis 的 homepage
      }
   ]

使用 composer 下载

composer  require  satis/package  //  这里注意下,名称使用你的私有包的里面  composer.json  的  name

可以看到 satis/package 包被下载了下来。这样就完成了私有仓库的搭建。

部署

在做私有库之前先部署一下,目前我使用 github action 进行部署的。因为 Satis 使用的是私有仓库,所以呢,当执行下面的命令的时候

php  vendor/bin/satis  build

需要对私有仓库授权,才可以从 vcs 拉取代码。所以如果使用 Github Action 需要手动输入账户密码。看下面的注释

name:  私有仓库构建
on:
 push:
 branches:
     -  master
jobs:
 deploy:
 runs-on:  ubuntu-latest
 steps:
 # checkout master 分支
 -  name:  Checkout
 uses:  actions/checkout@master
 # 设置 PHP
 -  name:  Setup  PHP  with  PECL  extension
 uses:  shivammathur/setup-php@v2
 with:
 php-version:  '7.4'
 tools:  composer:v2
 # 模拟输入输出
 -  name:  Install  expect
 run:  sudo  apt-get  install  -y  expect
 # 安装 composer 依赖
 -  name:  安装  composer  依赖
 run:  |
 composer  install
 echo  "打包私有库"
 expect  -c  "
          spawn php vendor/bin/satis build
         expect \"Username:\"
         send \"${{ secrets.GITEE_USERNAME  }}\r\" // username 我使用的 GITEE
         expect \"Password:\"
         send \"${{ secrets.GITEE_PASSWORD  }}\r\" // password 我使用的 GITEE
         expect -re \".*auth.*\"
         send \"Y\r\"
         expect eof
        "
 -  name:  部署私有库
     uses:  easingthemes/ssh-deploy@v5.0.0
     with:
     REMOTE_HOST:  ${{ secrets.SERVER_HOST  }}
     REMOTE_USER:  ${{ secrets.SERVER_USERNAME  }}
     SSH_PRIVATE_KEY:  ${{ secrets.SERVER_SECRET  }}
     ARGS:  -rlgoDzc  -i  --delete
     SOURCE:  'dist/'
     REMOTE_PORT:  ${{ secrets.SERVER_PORT  }}
     TARGET:  ${{ secrets.SERVER_WORKDIR  }}

当然我是做了自动部署,你也可以直接上传。使用 nginx 配置

这里如果配置 ssl,发现都无法使用 composer 拉取。如果使用 https,每个用户都需要在本地配置 ssl 证书,所以干脆直接就不用了 😂

server
{
 listen  80;
 server_name  satis.catchadmin.com;
 index  index.html  index.php  index.htm  default.php  default.htm  default.html;
 root  /var/www/html/satis; //  配置  root  path
 access_log /var/log/nginx/satis.log;
 error_log /var/log/nginx/satis.log;
}

可以直接当做静态项目访问就可以了

私有库认证

上面的还不够私有,一旦被别人发现配置,任何人都可以下载这个包。所以还需要接入一个认证系统。

在接入之前,你可以通过下面的命令来获取包的时候,就可以看到整个包的下载过程

composer  require  satis/package  -vvv

你会发现,整个包的过程是不经过框架的,都是直接访问 dist/ 下面的文件,这说明什么呢?说明这个项目有两种访问方式。

  • 经过 index.php

  • 不经过 index.php 直接访问 dist 下面的 zip 资源。

但是这两种方式都是通过 nginx 来访问的。如果在 nginx 上设置访问权限的话,是不是就可以了?简单的认证可以通过 http.basic,这也是 satis 推荐的认证方法。你可以在 nginx 设置如下配置


location  /
{
 auth_basic  "输入用户名密码";
 auth_basic_user_file  conf.d/authfile; //  认证文件地址
 autoindex  on;
}

authfile 里面可以配置用户密码,格式如下,一行一个

username:pwd

重启 nginx 之后,再试一次,再次尝试下载就需要输入用户名密码,才可以继续了。似乎已经完成了目标。必须认证通过才可以继续访问。但是这里会出现另外一个问题。

每次有新用户使用私有库的包,都要去修改 conf.d/authfile 配置吗?并且每次都需要重启 nginx。这样是不是很麻烦。如果不用 nginx 的 http.basic 这种特别费劳动力的方法,如何解决这个问题?

可以换一种方式思考这个问题。前提是要能接入自定义的认证系统。在这个前提下问题就变成了在访问 dist 目录下资源的时候,需要发送一次请求给认证系统,在认证系统通过之后就可以继续访问包资源。这样就很容易理解了,解决这个问题需要新认识 nginx 的一个指令。

auth_request

高版本的 nginx 是内置这个指令的,如果是低版本,则需要编译 nginx-auth-request-module 模块。

auth_request 指令的作用就是在访问资源的时候、发送一次子请求至认证系统,如果认证系统返回 2xx 的状态码,那么就可以继续访问资源的请求,如果返回 401 状态,则中断访问。

这个指令就完全符合这个需求。配置如下


location  /dist  {
 auth_request  /auth;
}

location  /auth  {
 internal;
 proxy_method POST;
 proxy_set_header Accept  "application/json";
 proxy_set_header X-Original-URI  $request_uri;
 proxy_pass https://xxxx.com/satis/auth; //  授权地址
}

看第一个 location 配置,访问 dist 目录时,通过 auth_request 发送一个子请求到认证系统的 /auth 下。

接着看第三个 location,不光请求接口,还将原请求的地址通过 X-Original-URI 一并发送给了接口,原请求地址就是 composer 包的地址。通过包地址可以知道一些包的信息,例如包名,版本号之类的。系统从这些信息中,当前判断用户是否有访问包的权限。

到这里还是会有疑问 🤔️。那用户的用户名和密码呢?从哪里获取呢。不急,先写好系统的认证接口。从认证接口里,将会知道一切!这里使用 Laravel,其他任何框架都是一样的

  • 先创建认证控制

php  artisan  make:controller  AuthController
  • 创建路由
cd  routes
vim  web.php
Route::post('/api/satis/authenticate', [\App\Http\Controllers\AuthController::class, 'index']);

由于 Laravel 会自带 CSRF,所以先关闭 CSRF,不然请求将会被拦截。

  • 认证代码

namespace  App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class  AuthController  extends  Controller
{
 //
 public  function  index(Request  $request)
 {
     // Log::info(json_encode($request->header()));
     $username  =  $request->header('php-auth-user');
     $pwd  =  $request->header('php-auth-pw');
     // $token = explode(' ', $request->header('authorization'))[1];
     // [$username, $pwd] = explode(':', base64_decode($token));
     $res  =  $username  ===  'xxx'  &&  $pwd  ===  'xxxx';
     abort_unless($res, 401, 'Auth Failed');
     return  response('Auth Successful', 200);
 }
}
  • 认证信息是通过 header 获取

  • 可以通过 headerphp-auth-userphp-auth-pw 获取用户名密码

  • 也可以 headerauthorization 获取,token 其实就是 username:pwd 字符 base64 之后的结果

  • 最后通过 usernamepwd 来认证

上面的结果只是一个非常简单的示例,需要更加完成的私有仓库方案,还需要完善其他的一些功能。目前想到的有

  • 提供添加仓库的功能

  • 自动获取私有包

  • 提供购买私有包的功能

原文链接

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 2

之前用lua写了个

server {
    listen 8899;
    server_name localhost;

    root /files/php/satis/tc-composers;
    index index.html index.htm; 

    location / {
        access_by_lua_block {
            local mysql = require "resty.mysql"
            local cjson = require "cjson"
            local ngx = require "ngx"
            ngx.header.content_type="application/json"

            local function get_auth_header()
                local auth_header = ngx.var.http_Authorization
                if not auth_header then
                    ngx.status = ngx.HTTP_UNAUTHORIZED
                    local error_message = cjson.encode({
                        error = "No Authorization header"
                    })
                    ngx.say(error_message)
                    ngx.exit(ngx.HTTP_UNAUTHORIZED)
                end
                return auth_header
            end

            local function get_routes_from_db(auth_header)
                local db, err = mysql:new()
                if not db then
                    local error_message = cjson.encode({
                        error = "failed to instantiate mysql: " .. err
                    })
                    ngx.say(error_message)
                    return
                end

                local ok, err, errno, sqlstate = db:connect{
                    host = "127.0.0.1",
                    port = 3306,
                    database = "test",
                    user = "root",
                    password = "123456",
                    charset = "utf8mb4",
                    max_packet_size = 1024 * 1024,
                }

                if not ok then
                    local error_message = cjson.encode({
                        error = "failed to connect: " .. err,
                        errno = errno,
                        sqlstate = sqlstate
                    })
                    ngx.say(error_message)
                    return
                end

                local res, err, errno, sqlstate =
                    db:query("SELECT type, route FROM permissions WHERE auth = "..ngx.quote_sql_str(auth_header))

                if not res then
                    local error_message = cjson.encode({
                        error = "bad result: " .. err,
                        errno = errno,
                        sqlstate = sqlstate
                    })
                    ngx.say(error_message)
                    return
                end

                local user_info = { routes = {}, type = nil }
                for i, row in ipairs(res) do
                    user_info.routes[row.route] = true
                    user_info.type = row.type
                end
                return user_info
            end

            local function check_permission(user_info)
                if user_info.type ~= 1 and not user_info.routes[ngx.var.uri] then
                    ngx.status = ngx.HTTP_FORBIDDEN
                    local error_message = cjson.encode({
                        error = "Access denied"
                    })
                    ngx.say(error_message)
                    ngx.exit(ngx.HTTP_FORBIDDEN)
                end
            end

            local auth_header = get_auth_header()
            local user_info = get_routes_from_db(auth_header)
            check_permission(user_info)
        }
        try_files $uri $uri/ /index.html?$query_string;
    }
    access_log  /var/log/nginx/satis.access.log;
    error_log   /var/log/nginx/satis.error.log;
}
2个月前 评论
JaguarJack (楼主) 2个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!