PHP转Go系列 | ThinkPHP与Gin框架之OpenApi授权设计实践

php,go,thinkphp,gin,openapi · 浏览次数 : 77

小编点评

本文主要讨论了在一个ToB业务公司的SaaS平台中,OpenAPI授权设计的实践经验。文章首先介绍了OpenAPI的基本概念和作用,然后详细阐述了如何为每个租户分配AppKey和AppSecret,以及如何通过这些凭据获取AccessToken和RefreshToken。接着,文章详细描述了如何在控制器中实现生成AccessToken、通过AccessToken换取RefreshToken和刷新RefreshToken的API接口。最后,文章还讨论了如何使用Gin框架搭建API服务,并通过Postman工具测试API接口。 1. **OpenAPI的概念和作用**:本文首先介绍了OpenAPI的基本概念,它是许多大型开放平台(如微信、淘宝、京东、抖音等)使用的API接口设计规范。OpenAPI允许开发者通过API获取第三方平台的数据,同时也提供了授权机制来保护API的安全。 2. **获取访问令牌和刷新令牌**:文章详细说明了如何为每个租户生成一对AppKey和AppSecret,并通过这些凭据获取平台颁发的AccessToken和RefreshToken。RefreshToken具有时效性,有效期设置为2小时,且只能通过刷新接口刷新,以降低RefreshToken被劫持的风险。 3. **API接口实现**:在控制器中,文章实现了三个关键API接口:生成AccessToken、通过AccessToken换取RefreshToken和刷新RefreshToken。这些接口使用了JWT规则来生成和验证Token。 4. **Gin框架的使用**:文章还展示了如何使用Gin框架搭建API服务,并通过Postman工具测试API接口。Gin是一个流行的Go语言Web框架,它提供了丰富的中间件和路由功能,方便开发者快速构建API服务。 5. **实践和代码获取**:文章最后鼓励读者通过实践来加深理解,并提供了获取完整实践代码的方法。

正文

大家好,我是码农先森。

我之前待过一个做 ToB 业务的公司,主要是研发以会员为中心的 SaaS 平台,其中涉及的子系统有会员系统、积分系统、营销系统等。在这个 SaaS 平台中有一个重要的角色「租户」,这个租户可以拥有一个或多个子系统的使用权限,此外租户还可以使用平台所提供的开放 API 「即 OpenApi」来获取相关系统的数据。有了 OpenApi 租户可以更便捷的与租户自有系统进行打通,提高系统之间数据的传输效率。那么这一次实践的主要内容是 OpenApi 的授权设计,希望对大家能有所帮助。

我们先梳理一下本次实践的关键步骤:

  • 给每一个租户分配一对 AppKey、AppSecret。
  • 租户通过传递 AppKey、AppSecret 参数获取到平台颁发的 AccessToken。
  • 租户再通过 AccessToken 来换取可以实际调用 API 的 RefreshToken。
  • 这时的 RefreshToken 是具有时效性,目前设置的有效期为 2 个小时。
  • 针对 RefreshToken 还会提供一个刷新时效的接口。
  • 只有 RefreshToken 才有调用业务 API 的真实权限。

有些朋友对 AccessToken 和 RefreshToken 傻傻分不清,疑问重重?我在最开始接触这个设计的时候也是懵逼的,为啥要搞两个,一个不也能解决问题吗?确实搞一个也可以用,但大家如果对接过微信的开放 API 就会发现他们也是有两个,此外还有很多大的开放平台也是采用类似的设计逻辑,所以存在即合理。

这里我说一下具体的原因,AccessToken 是基于 AppKey 和 AppSecret 来生成的,而 RefreshToken 是通过 AccessToken 交换得来的。并且 RefreshToken 具备有效性,需要通过一个刷新接口,不定时的刷新 RefreshToken。RefreshToken 的使用是最频繁的,在每次的业务 API 调用是都需要进行传输,传输的次数多了那么 RefreshToken 被劫持的风险就会变大。假设 RefreshToken 真的被泄露,那么损失也是控制在 2 个小时以内,为了减低损失也还可以调低有效时间。总而言之,网络的传输并不总是能保证安全,AccessToken 在网络上只需要一次传输「即换取 RefreshToken」,而 RefreshToken 需要不断的在网络的传输「即不断调用业务 API」,传输的次数越少风险就越低,这就是设计两个 Token 的根本原因。

话不多说,开整!

按照惯例,我们先对整个目录结构进行梳理。这次的重点逻辑主要是在控制器 controller 的 auth 中实现,包含三个 API 接口一是生成 AccessToken、二是通过 AccessToken 交换 RefreshToken,三是刷新 RefreshToken。中间件 middleware 的 api_auth 是对 RefreshToken 进行解码验证,判断客户端传递的 RefreshToken 是否有效。此外,AccessToken 和 RefreshToken 的生成策略都是采用的 JWT 规则。

[manongsen@root php_to_go]$ tree -L 2
.
├── go_openapi
│   ├── app
│   │   ├── controller
│   │   │   ├── auth.go
│   │   │   └── user.go
│   │   ├── middleware
│   │   │   └── api_auth.go
│   │   ├── model
│   │   │   └── tenant.go
│   │   ├── config
│   │   │   └── config.go
│   │   └── route.go
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── php_openapi
│   ├── app
│   │   ├── controller
│   │   │   ├── Auth.php
│   │   │   └── User.php
│   │   ├── middleware
│   │   │   └── ApiAuth.php
│   │   ├── model
│   │   │   └── Tenant.php
│   │   └── middleware.php
│   ├── composer.json
│   ├── composer.lock
│   ├── config
│   ├── route
│   │   └── app.php
│   ├── think
│   ├── vendor
│   └── .env

ThinkPHP

使用 composer 创建 php_openapi 项目,并且安装 predis、php-jwt 扩展包。

[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/php_openapi
[manongsen@root php_openapi]$ composer create-project topthink/think php_openapi
[manongsen@root php_openapi]$ cp .example.env .env

[manongsen@root php_openapi]$ composer require predis/predis
[manongsen@root php_openapi]$ composer require firebase/php-jwt

使用 ThinkPHP 框架提供的命令行工具 php think 创建控制器、中间件、模型文件。

[manongsen@root php_openapi]$ php think make:model Tenant
Model:app\model\Tenant created successfully.

[manongsen@root php_openapi]$ php think make:controller Auth
Controller:app\controller\Auth created successfully.

[manongsen@root php_openapi]$ php think make:controller User
Controller:app\controller\User created successfully.

[manongsen@root php_openapi]$ php think make:middleware ApiAuth
Middleware:app\middleware\ApiAuth created successfully.

在 route/app.php 文件中定义接口的路由。

<?php
use think\facade\Route;

Route::post('auth/access', 'auth/accessToken');
Route::post('auth/exchange', 'auth/exchangeToken');
Route::post('auth/refresh', 'auth/refreshToken');

// 指定使用 ApiAuth 中间件
Route::group('user', function () {
    Route::get('info', 'user/info');
})->middleware(\app\middleware\ApiAuth::class);

从下面这个控制器 Auth 文件可以看出有 accessToken()、exchangeToken()、refreshToken() 三个方法,分别对应的都是三个 API 接口。这里会使用 JWT 来生成 Token 令牌,然后统一存储到 Redis 缓存中。其中 accessToken 的有效时间通常会比 refreshToken 长,但在业务接口的实际调用中使用的是 refreshToken。

<?php

namespace app\controller;

use app\BaseController;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use app\model\Tenant;
use think\facade\Cache;
use think\facade\Env;

class Auth extends BaseController
{
    /**
     * 生成一个 AccessToken
     */
    public function accessToken()
    {
        // 获取 AppKey 和 AppSecret 参数
        $params = $this->request->param();
        if (!isset($params["app_key"])) {
            return json(["code" => 400, "msg" => "AppKey参数缺失"]);
        }
        $appKey = $params["app_key"];
        if (empty($appKey)) {
            return json(["code" => 400, "msg" => "AppKey参数为空"]);
        }

        if (!isset($params["app_secret"])) {
            return json(["code" => 400, "msg" => "AppSecret参数缺失"]);
        }
        $appSecret = $params["app_secret"];
        if (empty($appSecret)) {
            return json(["code" => 400, "msg" => "AppSecret参数为空"]);
        }

        // 在数据库中判断 AppKey 和 AppSecret 是否存在
        $tenant = Tenant::where('app_key', $appKey)->where('app_secret', $appSecret)->find();
        if (is_null($tenant)) {
            return json(["code" => 400, "msg" => "AppKey或AppSecret参数无效"]);
        }

        // 生成一个 AccessToken
        $expiresIn = 7 * 24 * 3600; // 7 天内有效
        $nowTime = time();
        $payload = [
            "iss" => "manongsen", // 签发者 可以为空
            "aud" => "tenant",    // 面向的用户,可以为空
            "iat" => $nowTime,    // 签发时间
            "nbf" => $nowTime,    // 生效时间
            "exp" => $nowTime + $expiresIn,  // AccessToken 过期时间
        ];
        $accessToken = JWT::encode($payload, $tenant->app_secret, "HS256");

        $scope = $tenant->scope;
        $data = [
            "access_token" => $accessToken, // 访问令牌
            "token_type"   => "bearer",     // 令牌类型
            "expires_in"   => $expiresIn,   // 过期时间,单位为秒
            "scope"        => $scope,       // 权限范围
        ];

        // 存储到 Redis
        $redis = Cache::store('redis')->handler();
        $redis->set(sprintf("%s.%s", Env::get("ACCESS_TOKEN_PREFIX"), $accessToken), $appKey, $expiresIn);

        return json_encode(["code" => 200, "msg"=>"ok", "data" => $data]);
    }

    /**
     * 通过 AccessToken 换取 RefreshToken
     */
    public function exchangeToken()
    {
        // 获取 AccessToken 参数
        $params = $this->request->param();
        if (!isset($params["access_token"])) {
            return json(["code" => 400, "msg" => "AccessToken参数缺失"]);
        }
        $accessToken = $params["access_token"];
        if (empty($accessToken)) {
            return json(["code" => 400, "msg" => "AccessToken参数为空"]);
        }

        // 校验 AccessToken
        $redis = Cache::store('redis')->handler();
        $appKey = $redis->get(sprintf("%s.%s", Env::get("ACCESS_TOKEN_PREFIX"), $accessToken));
        if (empty($appKey)) {
            return json(["code" => 400, "msg" => "AccessToken参数失效"]);
        }

        $tenant = Tenant::where('app_key', $appKey)->find();
        if (is_null($tenant)) {
            return json(["code" => 400, "msg" => "AccessToken参数失效"]);
        }

        $expiresIn = 2 * 3600; // 2 小时内有效
        $nowTime = time();
        $payload = [
            "iss" => "manongsen", // 签发者, 可以为空
            "aud" => "tenant",    // 面向的用户, 可以为空
            "iat" => $nowTime,    // 签发时间
            "nbf" => $nowTime,    // 生效时间
            "exp" => $nowTime + $expiresIn,  // RefreshToken 过期时间
        ];
        $refreshToken = JWT::encode($payload, $tenant->app_secret, "HS256");

        // 颁发 RefreshToken
        $data = [
            "refresh_token" => $refreshToken, // 刷新令牌
            "expires_in"    => $expiresIn,    // 过期时间,单位为秒
        ];

        // 存储到 Redis
        $redis = Cache::store('redis')->handler();
        $redis->set(sprintf("%s.%s", Env::get("REFRESH_TOKEN_PREFIX"), $refreshToken), $appKey, $expiresIn);

        return json_encode(["code" => 200, "msg"=>"ok", "data" => $data]);
    }

    /**
     * 刷新 RefreshToken
     */
    public function refreshToken()
    {
        // 获取 RefreshToken 参数
        $params = $this->request->param();
        if (!isset($params["refresh_token"])) {
            return json(["code" => 400, "msg" => "RefreshToken参数缺失"]);
        }
        $refreshToken = $params["refresh_token"];
        if (empty($refreshToken)) {
            return json(["code" => 400, "msg" => "RefreshToken参数为空"]);
        }

        // 校验 RefreshToken
        $redis = Cache::store('redis')->handler();
        $appKey = $redis->get(sprintf("%s.%s", Env::get("REFRESH_TOKEN_PREFIX"), $refreshToken));
        if (empty($appKey)) {
            return json(["code" => 400, "msg" => "RefreshToken参数失效"]);
        }

        $tenant = Tenant::where('app_key', $appKey)->find();
        if (is_null($tenant)) {
            return json(["code" => 400, "msg" => "RefreshToken参数失效"]);
        }

        // 颁发一个新的  RefreshToken
        $expiresIn = 2 * 3600; // 2 小时内有效
        $nowTime = time();
        $payload = [
            "iss" => "manongsen", // 签发者 可以为空
            "aud" => "tenant",    // 面向的用户,可以为空
            "iat" => $nowTime,    // 签发时间
            "nbf" => $nowTime,    // 生效时间
            "exp" => $nowTime + $expiresIn,  // RefreshToken 过期时间
        ];
        $newRefreshToken = JWT::encode($payload, $tenant->app_secret, "HS256");

        $data = [
            "refresh_token" => $newRefreshToken, // 新的刷新令牌
            "expires_in"    => $expiresIn,       // 过期时间,单位为秒
        ];

        // 将新的 RefreshToken 存储到 Redis
        $redis = Cache::store('redis')->handler();
        $redis->set(sprintf("%s.%s", Env::get("REFRESH_TOKEN_PREFIX"), $newRefreshToken), $appKey, $expiresIn);

        // 删除旧的 RefreshToken
        $redis->del(sprintf("%s.%s", Env::get("REFRESH_TOKEN_PREFIX"), $refreshToken));
        return json_encode(["code" => 200, "msg"=>"ok", "data" => $data]);
    }
}

启动 php_openapi 服务。

[manongsen@root php_openapi]$ php think run
ThinkPHP Development server is started On <http://0.0.0.0:8000/>
You can exit with `CTRL-C`
Document root is: /home/manongsen/workspace/php_to_go/php_openapi/public
[Wed Jul  3 22:02:16 2024] PHP 8.3.4 Development Server (http://0.0.0.0:8000) started

使用 Postman 工具在 Header 上设置 Authorization 参数「即 RefreshToken」便可以成功的返回数据。

Gin

使用 go mod init 初始化 go_openapi 项目,再使用 go get 安装相应的第三方依赖库。

[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/go_openapi
[manongsen@root go_openapi]$ go mod init go_openapi

[manongsen@root go_openapi]$ go get github.com/gin-gonic/gin
[manongsen@root go_openapi]$ go get gorm.io/gorm
[manongsen@root go_openapi]$ go get github.com/golang-jwt/jwt/v4
[manongsen@root go_openapi]$ go get github.com/go-redis/redis

在 Gin 中没有类似 php think 的命令行工具,因此需要自行创建 controller、middleware、model 等文件。

在 app/route.go 路由文件中定义接口,和在 ThinkPHP 中的使用差不多并无两样。

package app

import (
	"go_openapi/app/controller"
	"go_openapi/app/middleware"

	"github.com/gin-gonic/gin"
)

func InitRoutes(r *gin.Engine) {
	r.POST("/auth/access", controller.AccessToken)
	r.POST("/auth/exchange", controller.ExchangeToken)
	r.POST("/auth/refresh", controller.RefreshToken)

	// 指定使用 ApiAuth 中间件
	user := r.Group("/user/").Use(middleware.ApiAuth())
	user.GET("info", controller.UserInfo)
}

同样在 Gin 的控制器中也是三个方法对应三个接口。

package controller

import (
	"fmt"
	"go_openapi/app/config"
	"go_openapi/app/model"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
)

// 生成一个 AccessToken
func AccessToken(c *gin.Context) {
	// 获取 AppKey 和 appSecret 参数
	appKey := c.PostForm("app_key")
	if len(appKey) == 0 {
		c.JSON(http.StatusOK, gin.H{
			"code": 400,
			"msg":  "AppKey参数为空",
		})
		return
	}

	appSecret := c.PostForm("app_secret")
	if len(appSecret) == 0 {
		c.JSON(http.StatusOK, gin.H{
			"code": 400,
			"msg":  "appSecret参数为空",
		})
		return
	}

	// 在数据库中判断 AppKey 和 appSecret 是否存在
	var tenant *model.Tenant
	dbRes := config.DemoDB.Model(&model.Tenant{}).
		Where("app_key = ?", appKey).
		Where("app_secret = ?", appSecret).
		First(&tenant)
	if dbRes.Error != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "内部服务错误",
		})
		return
	}

	// 生成一个 AccessToken
	expiresIn := int64(7 * 24 * 3600) // 7 天内有效
	nowTime := time.Now().Unix()

	jwtToken := jwt.New(jwt.SigningMethodHS256)
	claims := jwtToken.Claims.(jwt.MapClaims)
	claims["iss"] = "manongsen"         // 签发者 可以为空
	claims["aud"] = "tenant"            // 面向的用户,可以为空
	claims["iat"] = nowTime             // 签发时间
	claims["nbf"] = nowTime             // 生效时间
	claims["exp"] = nowTime + expiresIn // AccessToken 过期时间
	accessToken, err := jwtToken.SignedString([]byte(tenant.AppSecret))
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "内部服务错误",
		})
		return
	}

	scope := tenant.Scope
	data := map[string]interface{}{
		"access_token": accessToken, // 访问令牌
		"token_type":   "bearer",    // 令牌类型
		"expires_in":   expiresIn,   // 过期时间,单位为秒
		"scope":        scope,       // 权限范围
	}

	// 存储 AccessToken 到 Redis
	config.RedisConn.Set(fmt.Sprintf("%s.%s", config.ACCESS_TOKEN_PREFIX, accessToken), tenant.AppKey, time.Second*time.Duration(expiresIn)).Result()
	c.JSON(http.StatusOK, gin.H{
		"code": 200,
		"msg":  "ok",
		"data": data,
	})
}

// 通过 AccessToken 换取 RefreshToken
func ExchangeToken(c *gin.Context) {
	// 获取 AccessToken 参数
	accessToken := c.PostForm("access_token")
	if len(accessToken) == 0 {
		c.JSON(http.StatusOK, gin.H{
			"code": 400,
			"msg":  "AccessToken参数为空",
		})
		return
	}

	// 校验 AccessToken
	appKey, err := config.RedisConn.Get(fmt.Sprintf("%s.%s", config.ACCESS_TOKEN_PREFIX, accessToken)).Result()
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "内部服务错误",
		})
		return
	}
	if len(appKey) == 0 {
		c.JSON(http.StatusOK, gin.H{
			"code": 400,
			"msg":  "AccessToken参数失效",
		})
		return
	}

	var tenant *model.Tenant
	dbRes := config.DemoDB.Model(&model.Tenant{}).
		Where("app_key = ?", appKey).
		First(&tenant)
	if dbRes.Error != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "内部服务错误",
		})
		return
	}

	expiresIn := int64(2 * 3600) // 2 小时内有效
	nowTime := time.Now().Unix()

	jwtToken := jwt.New(jwt.SigningMethodHS256)
	claims := jwtToken.Claims.(jwt.MapClaims)
	claims["iss"] = "manongsen"         // 签发者 可以为空
	claims["aud"] = "tenant"            // 面向的用户,可以为空
	claims["iat"] = nowTime             // 签发时间
	claims["nbf"] = nowTime             // 生效时间
	claims["exp"] = nowTime + expiresIn // RefreshToken 过期时间
	refreshToken, err := jwtToken.SignedString([]byte(tenant.AppSecret))
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "内部服务错误",
		})
		return
	}

	// 颁发 RefreshToken
	data := map[string]interface{}{
		"refresh_token": refreshToken, // 刷新令牌
		"expires_in":    expiresIn,    // 过期时间,单位为秒
	}

	// 存储到 Redis
	config.RedisConn.Set(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, refreshToken), appKey, time.Second*time.Duration(expiresIn))
	c.JSON(http.StatusOK, gin.H{
		"code": 200,
		"msg":  "ok",
		"data": data,
	})
}

// 刷新 RefreshToken
func RefreshToken(c *gin.Context) {
	// 获取 RefreshToken 参数
	refreshToken := c.PostForm("refresh_token")
	if len(refreshToken) == 0 {
		c.JSON(http.StatusOK, gin.H{
			"code": 400,
			"msg":  "RefreshToken参数为空",
		})
		return
	}

	// 校验 RefreshToken
	appKey, err := config.RedisConn.Get(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, refreshToken)).Result()
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "内部服务错误",
		})
	}
	if len(appKey) == 0 {
		c.JSON(http.StatusOK, gin.H{
			"code": 400,
			"msg":  "AccessToken参数失效",
		})
		return
	}

	var tenant *model.Tenant
	dbRes := config.DemoDB.Model(&model.Tenant{}).
		Where("app_key = ?", appKey).
		First(&tenant)
	if dbRes.Error != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "内部服务错误",
		})
		return
	}

	// 颁发一个新的  RefreshToken
	expiresIn := int64(2 * 3600) // 2 小时内有效
	nowTime := time.Now().Unix()

	jwtToken := jwt.New(jwt.SigningMethodHS256)
	claims := jwtToken.Claims.(jwt.MapClaims)
	claims["iss"] = "manongsen"         // 签发者 可以为空
	claims["aud"] = "tenant"            // 面向的用户,可以为空
	claims["iat"] = nowTime             // 签发时间
	claims["nbf"] = nowTime             // 生效时间
	claims["exp"] = nowTime + expiresIn // RefreshToken 过期时间
	newRefreshToken, err := jwtToken.SignedString([]byte(tenant.AppSecret))
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "内部服务错误",
		})
		return
	}

	data := map[string]interface{}{
		"refresh_token": newRefreshToken, // 新的刷新令牌
		"expires_in":    expiresIn,       // 过期时间,单位为秒
	}

	// 将新的 RefreshToken 存储到 Redis
	config.RedisConn.Set(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, newRefreshToken), appKey, time.Second*time.Duration(expiresIn))

	// 删除旧的 RefreshToken
	config.RedisConn.Del(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, refreshToken))
	c.JSON(http.StatusOK, gin.H{
		"code": 200,
		"msg":  "ok",
		"data": data,
	})
}

启动 go_openapi 服务。

[manongsen@root go_openapi]$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /auth/access              --> go_openapi/app/controller.AccessToken (3 handlers)
[GIN-debug] POST   /auth/exchange            --> go_openapi/app/controller.ExchangeToken (3 handlers)
[GIN-debug] POST   /auth/refresh             --> go_openapi/app/controller.RefreshToken (3 handlers)
[GIN-debug] GET    /user/info                --> go_openapi/app/controller.UserInfo (4 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8001

使用 Postman 工具在 Header 上设置 Authorization 参数「即 RefreshToken」便可以成功的返回数据。

结语

工作中只要接触过第三方开放平台的都离不开 OpenApi,几乎各大平台都会有自己的 OpenApi 比如微信、淘宝、京东、抖音等。在 OpenApi 对接的过程中最首要的环节就是授权,获取到平台的授权 Token 至关重要。对于我们程序员来说,不仅要能对接 OpenApi 获取到业务数据,还有对其中的授权实现逻辑要有具体的研究,才能通晓其本质做到一通百通。这次我分享的是基于之前公司做 SaaS 平台一些经验的提取,希望能对大家有所帮助。最好的学习就是实践,大家可以手动实践一下,如有需要完整实践代码的朋友可在微信公众号内回复「1087」获取对应的代码。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

与PHP转Go系列 | ThinkPHP与Gin框架之OpenApi授权设计实践相似的内容:

PHP转Go系列 | ThinkPHP与Gin框架之OpenApi授权设计实践

工作中只要接触过第三方开放平台的都离不开 OpenApi,几乎各大平台都会有自己的 OpenApi 比如微信、淘宝、京东、抖音等。在 OpenApi 对接的过程中最首要的环节就是授权,获取到平台的授权 Token 至关重要。

PHP转Go系列 | ThinkPHP与Gin框架之API接口签名设计实践

大家好,我是码农先森。 回想起以前用模版渲染数据的岁月,那时都没有 API 接口开发的概念。PHP 服务端和前端 HTML、CSS、JS 代码混合式开发,也不分前端、后端程序员,大家都是全干工程师。随着前后端分离、移动端开发的兴起,用后端渲染数据的开发方式效率低下,已经不能满足业务对需求快速上线的要

PHP转Go系列 | GET 和 POST 请求的使用姿势

大家好,我是码农先森。 说到 HTTP 请求工具想必对我们做 Web 开发的程序员都不陌生,只要涉及到网络请求都必须使用。对于我们 PHP 程序员来说,最熟悉不过的就是 CURL 扩展,只要安装的这个扩展便可随意发起 HTTP 请求。 但在 PHP 语言中还有一个很好用的 Composer 包「gu

PHP 程序员转 Go 语言的经历分享

大家好,我是码农先森。 之前有朋友让我分享从 PHP 转 Go 的经历,这次它来了。我主要从模仿、进阶、应用这三个方面来描述转 Go 的经历及心得。模仿是良好的开端,进阶是艰难的成长,应用是认知的提升。希望我的经历对大家能有所启发。 模仿 著名艺术家毕加索说过「模仿是人类一切学习的开端,然后才是创新

[转帖]@nginx多server及使用优化(php)

文章目录​ ​一、nginx多server优先级​​​ ​二、禁止IP访问页面​​​ ​三、nginx的包含include​​​ ​四、nginx 路径的alias和root​​​ ​1.配置​​​ ​2.总结​​​ ​五、nginx的try_files​​​ ​1.配置try_files​​​ ​

[转帖]PAM4技术

http://www.xc66.cc/baike/view.php?id=b9f94c77652c9a76fc8a442748cd54bd PAM4技术本质是一种更高效的调制技术,可以有效提升带宽利用效率。 中文全称:四阶脉冲幅度调制 英文全称:Pulse Amplitude Modulation

[转帖]CDMA

http://www.xc66.cc/baike/view.php?id=3c1e4bd67169b8153e0047536c9f541e 学习之 收藏 | 点赞(0) | 踩(0) | | | 编辑 码分多址(CDMA)是在数字技术的分支--扩频通信技术上发展起来的一种崭新而成熟的无线通信技术。

[转帖]光刻技术

http://www.xc66.cc/baike/view.php?id=97d0145823aeb8ed80617be62e08bdcc 光刻技术为一种精密的微细加工技术。 中文全称:光刻技术 英文全称:Photetch 光刻技术是现代半导体、微电子、信息产业的基础,光刻技术直接决定了这些技术的发

[转帖]丢包

http://www.xc66.cc/baike/view.php?id=712a3c9878efeae8ff06d57432016ceb 丢包指数据包由于各种原因在信道中丢失的现象。 中文全称:丢包 英文全称:Packet loss 简称:Packet loss 简介:丢包(Packet loss

[转帖]骨干网

http://www.xc66.cc/baike/view.php?id=ef2a4be5473ab0b3cc286e67b1f59f44 骨干网(Backbone Network)是用来连接多个区域或地区的高速网络。每个骨干网中至少有一个和其他骨干网进行互联互通的连接点。不同的网络供应商都拥有自己