当前位置: 首页 > news >正文

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

大家好,我是码农先森。

我之前待过一个做 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。

<?phpnamespace 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 appimport ("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 controllerimport ("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.TenantdbRes := 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}// 生成一个 AccessTokenexpiresIn := 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.Scopedata := map[string]interface{}{"access_token": accessToken, // 访问令牌"token_type":   "bearer",    // 令牌类型"expires_in":   expiresIn,   // 过期时间,单位为秒"scope":        scope,       // 权限范围}// 存储 AccessToken 到 Redisconfig.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}// 校验 AccessTokenappKey, 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.TenantdbRes := 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}// 颁发 RefreshTokendata := map[string]interface{}{"refresh_token": refreshToken, // 刷新令牌"expires_in":    expiresIn,    // 过期时间,单位为秒}// 存储到 Redisconfig.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}// 校验 RefreshTokenappKey, 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.TenantdbRes := 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}// 颁发一个新的  RefreshTokenexpiresIn := 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 存储到 Redisconfig.RedisConn.Set(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, newRefreshToken), appKey, time.Second*time.Duration(expiresIn))// 删除旧的 RefreshTokenconfig.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授权设计实践

大家好&#xff0c;我是码农先森。 我之前待过一个做 ToB 业务的公司&#xff0c;主要是研发以会员为中心的 SaaS 平台&#xff0c;其中涉及的子系统有会员系统、积分系统、营销系统等。在这个 SaaS 平台中有一个重要的角色「租户」&#xff0c;这个租户可以拥有一个或多个子系…...

使用SOAP与TrinityCore交互(待定)

原文&#xff1a;SOAP with TrinityCore | TrinityCore MMo Project Wiki 如何使用SOAP与TC交互 SOAP代表简单对象访问协议&#xff0c;是一种类似于REST的基于标准的web服务访问协议的旧形式。只要必要的配置到位&#xff0c;您就可以利用SOAP向TrinityCore服务器发送命令。 …...

QQ频道导航退出

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/140413538 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…...

MySQL里的累计求和

在MySQL中&#xff0c;你可以使用SUM()函数来进行累计求和。如果你想要对一个列进行累计求和&#xff0c;可以使用OVER()子句与ORDER BY子句结合&#xff0c;进行窗口函数的操作。 以下是一个简单的例子&#xff0c;假设我们有一个名为sales的表&#xff0c;它有两个列&#x…...

Python爬虫速成之路(3):下载图片

hello hello~ &#xff0c;这里是绝命Coding——老白~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#xff1a;绝命Coding-CSDN博客 &a…...

同三维T80004EA编解码器视频使用操作说明书:高清HDMI编解码器,高清SDI编解码器,4K超清HDMI编解码器,双路4K超高清编解码器

同三维T80004EA编解码器视频使用操作说明书&#xff1a;高清HDMI编解码器&#xff0c;高清SDI编解码器&#xff0c;4K超清HDMI编解码器&#xff0c;双路4K超高清编解码器 同三维T80004EA编解码器视频使用操作说明书&#xff1a;高清HDMI编解码器&#xff0c;高清SDI编解码器&am…...

ChatGPT提问获取高质量答案的艺术PDF下载书籍推荐分享

ChatGPT高质量prompt技巧分享pdf&#xff0c; ChatGPT提问获取高质量答案的艺术pdf。本书是一本全面的指南&#xff0c;介绍了各种 Prompt 技术的理解和利用&#xff0c;用于从 ChatGPTmiki sharing中生成高质量的答案。我们将探讨如何使用不同的 Prompt 工程技术来实现不同的目…...

微信小程序中的数据通信

方法1: 使用回调函数 在app.js中:可以在修改globalData后执行一个回调函数,这个回调函数可以是页面传递给app的一个更新函数。// app.js App({globalData: {someData: ,},setSomeData(newData, callback) {this.globalData.someData = newData;if (typeof callback === funct…...

everything搜索不到任何文件-设置

版本&#xff1a; V1.4.1.1024 (x64) 问题&#xff1a;搜索不到任何文件 click:[工具]->[选项]->下图所示 将本地磁盘都选中包含...

python如何结束程序运行

方法1&#xff1a;采用sys.exit(0)&#xff0c;正常终止程序&#xff0c;从图中可以看到&#xff0c;程序终止后shell运行不受影响。 方法2&#xff1a;采用os._exit(0)关闭整个shell&#xff0c;从图中看到&#xff0c;调用sys._exit(0)后整个shell都重启了&#xff08;RESTAR…...

InnoDB

InnoDB 是 MySQL 默认的存储引擎&#xff0c;它提供了事务支持、行级锁定和外键约束等高级功能。下面详细解析 InnoDB 的一些底层原理和关键特性。 1. 数据存储结构 表空间&#xff08;Tablespace&#xff09; InnoDB 使用表空间来管理数据存储&#xff0c;表空间可以是共享…...

spark运行报错:Container killed by YARN for exceeding memory limits

用spark跑数据量大的离线调度任务报错&#xff1a;Reason: Container killed by YARN for exceeding memory limits. 19.0 GB of 19 GB physical memory used. Consider boosting spark.yarn.executor.memoryOverhead or disabling yarn.nodemanager.vmem-check-enabled becaus…...

(三)大模型/人工智能/机器学习/深度学习/NLP

一.模型 模型&#xff0c;简单来说&#xff0c;就是用来表示或解释某个事物、现象或系统的一种工具或框架。它可以是实体的&#xff0c;也可以是虚拟的&#xff0c;目的是为了帮助我们更好地理解和预测所描述的对象。在生活中&#xff0c;模型无处不在&#xff0c;它们以各种形…...

数学基础 -- 三角学

三角学 三角学&#xff08;Trigonometry&#xff09;是数学的一个分支&#xff0c;主要研究三角形的边长与角度之间的关系。三角学在几何学、物理学、工程学等多个领域中有广泛的应用。以下是三角学的一些基本概念和公式&#xff1a; 基本概念 直角三角形&#xff1a;一个角…...

基于BitMap的工作日间隔计算

背景问题 在我们实际开发过程中&#xff0c;时常会遇到日期的间隔计算&#xff0c;即计算多少工作日之后的日期&#xff0c;在不考虑法定节假日的情况下也不是那么复杂&#xff0c;毕竟周六、周日是相对固定的&#xff0c;Java语言也提供了丰富的类来处理此问题。 然而&#x…...

sqlite3 — DB-API 2.0 interface for SQLite databases

sqlite3 — DB-API 2.0 interface for SQLite databases — Python 3.12.4 documentation sqlite3 — DB-API 2.0 interface for SQLite databasessqlite3 — SQLite数据库的DB-API 2.0接口 Source code: Lib/sqlite3/ 源代码位置&#xff1a;Lib/sqlite3/ SQLite is a C…...

Spring Boot中的安全配置与实现

Spring Boot中的安全配置与实现 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将深入探讨Spring Boot中的安全配置与实现&#xff0c;看看如何保护你的…...

DepthAnything(2): 基于ONNXRuntime在ARM(aarch64)平台部署DepthAnything

DepthAnything(1): 先跑一跑Depth Anything_depth anything离线怎么跑-CSDN博客 目录 1. 写在前面 2. 安装推理组件 3. 生成ONNX 4. 准备ONNXRuntime库 5. API介绍 6. 例程 1. 写在前面 DepthAnything是一种能在任何情况下处理任何图像的简单却又强大的深度估计模型。 …...

JAVA简单封装UserUtil

目录 思路 一、TokenFilterConfiguration 二、FilterConfig 三、TokenContextHolder 四、TokenUtil 五、UserUtil 思路 配置Token过滤器(TokenFilterConfiguration)&#xff1a;实现一个Token过滤器配置&#xff0c;用于拦截HTTP请求&#xff0c;从请求头中提取Token&…...

【TOOLS】Chrome扩展开发

Chrome Extension Development 1. 入门教程 入门案例&#xff0c;可以访问【 谷歌插件官网官方文档 】查看官方入门教程&#xff0c;这里主要讲解大概步骤 Chrome Extenson 没有固定的脚手架&#xff0c;所以项目的搭建需要根据开发者自己根据需求搭建项目&#xff08;例如通过…...

分享WPF的UI开源库

文章目录 前言一、HandyControl二、AduSkin三、Adonis UI四、Panuon.WPF.UI五、LayUI-WPF六、MahApps.Metro七、MaterialDesignInXamlToolkit八、FluentWPF九、DMSkin总结 前言 分享WPF的UI开源库。 一、HandyControl HandyControl是一套WPF控件库&#xff0c;它几乎重写了所…...

[ACM独立出版]2024年虚拟现实、图像和信号处理国际学术会议(ICVISP 2024)

最新消息ICVISP 2024-已通过ACM出版申请投稿免费参会&#xff0c;口头汇报或海报展示(可获得相应证明证书) ————————————————————————————————————————— [ACM独立出版]2024年虚拟现实、图像和信号处理国际学术会议&#xff08;ICVI…...

JVM:类加载器

文章目录 一、什么是类加载器二、类加载器的应用场景三、类加载器的分类1、分类2、启动类加载器3、Java中的默认类加载器&#xff08;1&#xff09;扩展类加载器&#xff08;2&#xff09;应用程序类加载器&#xff08;3&#xff09;arthas中类加载器相关的功能 四、双亲委派机…...

支持向量机 (support vector machine,SVM)

支持向量机 &#xff08;support vector machine&#xff0c;SVM&#xff09; flyfish 支持向量机是一种用于分类和回归的机器学习模型。在分类任务中&#xff0c;SVM试图找到一个最佳的分隔超平面&#xff0c;使得不同类别的数据点在空间中被尽可能宽的间隔分开。 超平面方…...

宝塔面板以www用户运行composer

方式一 执行命令时指定www用户 sudo -u www composer update方式二 在网站配置中的composer选项卡中选择配置运行...

昇思25天打卡营-mindspore-ML- Day24-基于 MindSpore 实现 BERT 对话情绪识别

学习笔记&#xff1a;基于MindSpore实现BERT对话情绪识别 算法原理 BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是由Google于2018年开发的一种预训练语言表示模型。BERT的核心原理是通过在大量文本上预训练深度双向表示&#xff0…...

【精品资料】模块化数据中心解决方案(33页PPT)

引言&#xff1a;模块化数据中心解决方案是一种创新的数据中心设计和部署策略&#xff0c;旨在提高数据中心的灵活性、可扩展性和效率。这种方案通过将数据中心的基础设施、计算、存储和网络资源封装到标准化的模块中&#xff0c;实现了快速部署、易于管理和高效运维的目标 方案…...

N6 word2vec文本分类

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊# 前言 前言 上周学习了训练word2vec模型&#xff0c;这周进行相关实战 1. 导入所需库和设备配置 import torch import torch.nn as nn import torchvision …...

excel、word、ppt 下载安装步骤整理

请按照我的步骤开始操作&#xff0c;注意以下截图红框标记处&#xff08;往往都是需要点击的地方&#xff09; 第一步&#xff1a;下载 首先进入office下载网址&#xff1a; otp.landian.vip 然后点击下载 拉到下方 下载站点&#xff08;这里根据自己的需要选择下载&#x…...

【python学习】标准库之日期和时间库定义、功能、使用场景和示例

引言 datetime模块最初是由 Alex Martelli 在 Python 2.3 版本引入的&#xff0c;目的是为了解决之前版本中处理日期和时间时存在的限制和不便 在datetime模块出现之前&#xff0c;Python 主要使用time模块来处理时间相关的功能&#xff0c;但 time模块主要基于 Unix 纪元时间&…...