PHP轉Go系列 | ThinkPHP與Gin框架之OpenApi授權設計實踐

来源:https://www.cnblogs.com/yxhblogs/p/18293238
-Advertisement-
Play Games

工作中只要接觸過第三方開放平臺的都離不開 OpenApi,幾乎各大平臺都會有自己的 OpenApi 比如微信、淘寶、京東、抖音等。在 OpenApi 對接的過程中最首要的環節就是授權,獲取到平臺的授權 Token 至關重要。 ...


大家好,我是碼農先森。

我之前待過一個做 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」獲取對應的代碼。


歡迎關註、分享、點贊、收藏、在看,我是微信公眾號「碼農先森」作者。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 條形碼和二維碼是現代信息交換和數據存儲的重要工具,它們將信息以圖形的形式編碼,便於機器識別和數據處理,被廣泛應用於物流、零售、醫療、教育等各領域。本文將介紹如何使用Python快速生成各種常見的條形碼如Code 128、EAN-13,以及生成二維碼。 Python條碼庫 本文需要用到 Spire.B ...
  • 以下是一個詳細全面的 Spring Boot 使用 WebSocket 的知識點彙總 1. 配置 WebSocket 添加依賴 進入maven官網, 搜索spring-boot-starter-websocket,選擇版本, 然後把依賴複製到pom.xml的dependencies標簽中 配置 We ...
  • 引言 在現代的互聯網應用中,數據安全和隱私保護變得越來越重要。尤其是在介面返回數據時,如何有效地對敏感數據進行脫敏處理,是每個開發者都需要關註的問題。本文將通過一個簡單的Spring Boot項目,介紹如何實現介面數據脫敏。 一、介面數據脫敏概述 1.1 介面數據脫敏的定義 介面數據脫敏是指在介面返 ...
  • 本文介紹基於R語言中的UBL包,讀取.csv格式的Excel表格文件,實現SMOTE演算法與SMOGN演算法,對機器學習、深度學習回歸中,訓練數據集不平衡的情況加以解決的具體方法~ ...
  • 什麼是I8n 國際化(I18n)指的是設計和開發產品的過程,使得它們能夠適應多種語言和文化環境,而不需要進行大量的代碼更改。這通常涉及到創建一個基礎版本的產品,然後通過配置和資源文件來添加對不同語言和地區的支持。 這樣,當產品需要在新的地理區域或語言環境中使用時,只需要添加或更新相應的資源文件,而不 ...
  • 目錄<future>future模板類成員函數:promise類promise的使用常式:packaged_task模板類常式:async模板函數常式:shared_future模板類 <future> 標準庫提供了一些工具來獲取非同步任務(即在單獨的線程中啟動的函數)的返回值,並捕捉其所拋出的異常。 ...
  • 目錄<condition_variable>condition_variable類類方法生產者消費者模型 -- 阻塞隊列單條件變數版condition_variable_any模板類區別優缺點 <condition_variable> 條件變數是C++11提供的另外一種用於等待的同步機制,它能阻塞一 ...
  • 大家好,我是 Java陳序員。 我們在日常生活中,有時候因為工作需要,需要發佈一些問卷調查,來統計數據,獲得反饋! 今天,給大家介紹一款開箱即用的開源問卷調查系統! 關註微信公眾號:【Java陳序員】,獲取開源項目分享、AI副業分享、超200本經典電腦電子書籍等。 項目介紹 TDuck —— 一款 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...