├── .gitignore ├── src ├── Exceptions │ ├── AccessKeyException.php │ ├── InvalidTokenException.php │ └── SignatureMethodException.php ├── Signatures │ ├── SignatureInterface.php │ └── Md5.php ├── fn.php ├── Command.php ├── config.php ├── ServiceProvider.php └── Middleware.php ├── composer.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /src/Exceptions/AccessKeyException.php: -------------------------------------------------------------------------------- 1 | 0; $i--) { 17 | $string .= $char[mt_rand(0, strlen($char) - 1)]; 18 | } 19 | 20 | return $string; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | info('access_key: ' . str_rand()); 31 | $this->info('secret_key: ' . str_rand()); 32 | } 33 | } -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | Middleware::STATUS_ON, // 状态,LaravelApiAuth::STATUS_ON 或者 LaravelApiAuth::STATUS_OFF 7 | 8 | 'roles' => [ 9 | // '{access_key}' => [ 10 | // 'name' => '{role_name}', // 角色名字,例如 android 11 | // 'secret_key' => '{secret_key}', 12 | // ], 13 | ], 14 | 15 | 'signature_methods' => [ 16 | 'md5' => \Qbhy\LaravelApiAuth\Signatures\Md5::class, 17 | ], 18 | 19 | 'skip' => [ 20 | 'is' => [Middleware::class, 'default_excludes_handler'], 21 | 'urls' => [], 22 | ], 23 | 24 | 'timeout' => 60, // 签名失效时间,单位: 秒 25 | ]; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "96qbhy/laravel-api-auth", 3 | "description": "laravel API 鉴权", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "96qbhy", 8 | "email": "96qbhy@gmail.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Qbhy\\LaravelApiAuth\\": "src" 14 | }, 15 | "files": [ 16 | "src/fn.php" 17 | ] 18 | }, 19 | "require": {}, 20 | "require-dev": { 21 | "laravel/laravel": "^5.5" 22 | }, 23 | "extra": { 24 | "laravel": { 25 | "providers": [ 26 | "Qbhy\\LaravelApiAuth\\ServiceProvider" 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | setupConfig(); 14 | } 15 | 16 | /** 17 | * Setup the config. 18 | */ 19 | protected function setupConfig() 20 | { 21 | $configSource = realpath(__DIR__ . '/config.php'); 22 | if ($this->app instanceof LaravelApplication && $this->app->runningInConsole()) { 23 | $this->publishes([ 24 | $configSource => config_path('api_auth.php') 25 | ]); 26 | } elseif ($this->app instanceof LumenApplication) { 27 | $this->app->configure('api_auth'); 28 | } 29 | $this->mergeConfigFrom($configSource, 'api_auth'); 30 | $this->commands([ 31 | Command::class, 32 | ]); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # laravel-api-auth 2 | laravel API 鉴权 3 | 4 | 这是一个 laravel 的 API 鉴权包, `laravel-api-auth` 采用 `jwt token` 的鉴权方式,只要客户端不被反编译从而泄露密钥,该鉴权方式理论上来说是安全的。 5 | PS: web 前端 API 没有绝对的安全,该项目的本意是给不暴露源码的客户端提供一种鉴权方案(如 service、APP客户端)。 6 | 7 | ## 安装 8 | ```bash 9 | composer require 96qbhy/laravel-api-auth 10 | ``` 11 | 12 | ## 配置 13 | 1. 注册 `ServiceProvider`: 14 | ```php 15 | Qbhy\LaravelApiAuth\ServiceProvider::class, 16 | ``` 17 | > laravel 5.5+ 版本不需要手动注册 18 | 19 | 2. 发布配置文件 20 | ```php 21 | php artisan vendor:publish --provider="Qbhy\LaravelApiAuth\ServiceProvider" 22 | ``` 23 | 24 | 3. 在 `App\Http\Kernal` 中注册中间件 25 | ```php 26 | protected $routeMiddleware = [ 27 | 'api_auth' => \Qbhy\LaravelApiAuth\Middleware::class, 28 | // other ... 29 | ]; 30 | ``` 31 | 32 | 4. 添加 `role` 33 | ```php 34 | php artisan api_auth 35 | ``` 36 | 然后按照格式把 `access_key` 和 `secret_key` 添加到, `config/api_auth.php` 里面的 `roles` 数组中。 37 | ```php 38 | 'roles' => [ 39 | '{access_key}' => [ 40 | 'name' => '{role_name}', // 角色名字,例如 android 41 | 'secret_key' => '{secret_key}', 42 | ], 43 | ], 44 | ``` 45 | 46 | 5. 自定义签名方法 (可选) 47 | `config/api_auth.php` 中的 `signature_methods` 可以添加自定义的签名类,该类需要继承自 `Qbhy\LaravelApiAuth\Signatures\SignatureInterface` 接口 48 | ```php 49 | get('client_role'); 85 | // todo... 86 | })->middleware(['api_auth']); 87 | 88 | \\ or 89 | 90 | Route::group(['middleware'=>'api_auth'], function(){ 91 | // routes... 92 | }); 93 | ``` 94 | > 通过验证后 `$request` 会添加一个 `client_role` 字段,该字段为客户端的角色名称。 95 | 96 | ### 前端 97 | ```javascript 98 | import axios from 'axios'; 99 | import { Base64 } from 'js-base64'; 100 | 101 | const access_key = '{access_key}'; // 服务端生成的 access_key 102 | const secret_key = '{secret_key}'; // 服务端生成的 secret_key 103 | 104 | const timestamp = Date.parse(new Date()) / 1000; // 取时间戳 105 | const echostr = 'asldjaksdjlkjgqpojg64131321'; // 随机字符串自行生成 106 | 107 | const header = Base64.encode(JSON.stringify({ 108 | "alg": "md5", 109 | "type": "jwt" 110 | })); 111 | const payload = Base64.encode(JSON.stringify({ 112 | "timestamp": timestamp, 113 | "echostr": echostr, 114 | "ak": access_key 115 | })); 116 | const signature_string = header + '.' + payload; 117 | 118 | function md5Sign(string, secret){ 119 | return md5(string + secret); // md5 库自行引入 120 | } 121 | 122 | const api_token = signature_string + '.' + md5Sign(signature_string,secret_key); 123 | 124 | const requestConfig = { 125 | headers: { 126 | "api-token": api_token 127 | } 128 | }; 129 | 130 | axios.post('/api/example',{},requestConfig).then(res=>{ 131 | // todo 132 | }); 133 | ``` 134 | > 本例子为 `web` 前端的例子,其他客户端同理,生成签名并且带上指定参数即可正常请求。 135 | > 通过自定义签名方法和自定义校验方法,可以使用其他加密方法进行签名,例如 `哈希` 等其他加密算法。 136 | 137 | 138 | 139 | [96qbhy.com](https://96qbhy.com) 140 | 96qbhy@gmail.com 141 | -------------------------------------------------------------------------------- /src/Middleware.php: -------------------------------------------------------------------------------- 1 | config = config('api_auth'); 20 | } 21 | 22 | /** 23 | * @param Request $request 24 | * @param \Closure $next 25 | * 26 | * @return mixed 27 | * @throws \Qbhy\LaravelApiAuth\Exceptions\AccessKeyException 28 | * @throws \Qbhy\LaravelApiAuth\Exceptions\InvalidTokenException 29 | * @throws \Qbhy\LaravelApiAuth\Exceptions\SignatureMethodException 30 | */ 31 | public function handle($request, Closure $next) 32 | { 33 | if ($this->config['status'] === static::STATUS_ON && !$this->is_skip($request)) { 34 | 35 | // 得到 api token 36 | $token = $request->hasHeader('api-token') ? $request->header('api-token') : $request->get('api-token'); 37 | 38 | // 得到 header 、 payload 、 signature 三段字符串 39 | list($header_string, $payload_string, $signature) = explode(".", $token); 40 | 41 | list($header, $payload, $alg) = $this->parseParams($header_string, $payload_string); 42 | 43 | $role = $this->config['roles'][$payload['ak']]; 44 | 45 | // 检查签名是否正确 46 | $this->signatureCheck($alg, "$header_string.$payload_string", $role['secret_key'], $signature); 47 | 48 | $request = $this->bindParamsToRequest($request, $role['name'], $payload); 49 | } 50 | 51 | return $next($request); 52 | } 53 | 54 | /** 55 | * 各种参数校验和解析 56 | * 57 | * @param string $header_string 58 | * @param string $payload_string 59 | * 60 | * @return array 61 | * @throws AccessKeyException 62 | * @throws InvalidTokenException 63 | * @throws SignatureMethodException 64 | */ 65 | public function parseParams(string $header_string, string $payload_string): array 66 | { 67 | // 检查参数 --begin 68 | $header = @json_decode(base64_decode($header_string), true); 69 | $payload = @json_decode(base64_decode($payload_string), true); 70 | 71 | if (!is_array($header) || 72 | !isset($header['alg']) || 73 | !is_array($payload) || 74 | !isset($payload['timestamp']) || 75 | !isset($payload['echostr']) || 76 | !isset($payload['ak']) 77 | ) { 78 | throw new InvalidTokenException('invalid token !'); 79 | } 80 | 81 | if (!isset($this->config['roles'][$payload['ak']])) { 82 | throw new AccessKeyException('access key invalid !'); 83 | } 84 | 85 | if (!isset($this->config['signature_methods'][$header['alg']])) { 86 | throw new SignatureMethodException($header['alg'] . ' signatures are not supported !'); 87 | } 88 | 89 | $alg = $this->config['signature_methods'][$header['alg']]; 90 | 91 | if (!class_exists($alg)) { 92 | throw new SignatureMethodException($header['alg'] . ' signatures method configuration error !'); 93 | } 94 | 95 | $alg = new $alg; 96 | 97 | if (!$alg instanceof SignatureInterface) { 98 | throw new SignatureMethodException($header['alg'] . ' signatures method configuration error !'); 99 | } 100 | 101 | // 检查参数 --end 102 | 103 | return compact('header', 'payload', 'alg'); 104 | } 105 | 106 | /** 107 | * 校验签名是否正确 108 | * 109 | * @param SignatureInterface $alg 110 | * @param string $signature_string 111 | * @param string $secret 112 | * @param $signature 113 | * 114 | * @throws InvalidTokenException 115 | */ 116 | public function signatureCheck(SignatureInterface $alg, string $signature_string, string $secret, $signature): void 117 | { 118 | if (!$alg::check($signature_string, $secret, $signature)) { 119 | throw new InvalidTokenException('invalid token !'); 120 | } 121 | } 122 | 123 | /** 124 | * @param Request $request 125 | * @param string $role_name 126 | * @param array $payload 127 | * 128 | * @return Request 129 | */ 130 | public function bindParamsToRequest($request, string $role_name, array $payload) 131 | { 132 | // 添加 role_name 到 $request 中 133 | if ($request->has('client_role')) { 134 | $request->offsetSet('_client_role', $request->get('client_role')); 135 | } 136 | $request->offsetSet('client_role', $role_name); 137 | 138 | // 添加 api_payload 到 $request 中 139 | if ($request->has('api_payload')) { 140 | $request->offsetSet('_api_payload', $request->get('api_payload')); 141 | } 142 | $request->offsetSet('api_payload', $payload); 143 | 144 | return $request; 145 | } 146 | 147 | public function is_skip(Request $request): bool 148 | { 149 | $handler = [static::class, 'default_skip_handler']; 150 | 151 | if (is_callable($this->config['skip']['is'])) { 152 | $handler = $this->config['skip']['is']; 153 | } 154 | 155 | return call_user_func_array($handler, [$request, $this->config['skip']['urls']]); 156 | } 157 | 158 | /** 159 | * @param Request $request 160 | * @param array $urls 161 | * 162 | * @return bool 163 | */ 164 | public static function default_skip_handler(Request $request, array $urls = []): bool 165 | { 166 | if (in_array($request->url(), $urls)) { 167 | return true; 168 | } 169 | 170 | return false; 171 | } 172 | 173 | } --------------------------------------------------------------------------------