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授权设计实践
大家好,我是码农先森。 我之前待过一个做 ToB 业务的公司,主要是研发以会员为中心的 SaaS 平台,其中涉及的子系统有会员系统、积分系统、营销系统等。在这个 SaaS 平台中有一个重要的角色「租户」,这个租户可以拥有一个或多个子系…...
使用SOAP与TrinityCore交互(待定)
原文:SOAP with TrinityCore | TrinityCore MMo Project Wiki 如何使用SOAP与TC交互 SOAP代表简单对象访问协议,是一种类似于REST的基于标准的web服务访问协议的旧形式。只要必要的配置到位,您就可以利用SOAP向TrinityCore服务器发送命令。 …...

QQ频道导航退出
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/140413538 长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV…...

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

Python爬虫速成之路(3):下载图片
hello hello~ ,这里是绝命Coding——老白~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹 💥个人主页:绝命Coding-CSDN博客 &a…...
同三维T80004EA编解码器视频使用操作说明书:高清HDMI编解码器,高清SDI编解码器,4K超清HDMI编解码器,双路4K超高清编解码器
同三维T80004EA编解码器视频使用操作说明书:高清HDMI编解码器,高清SDI编解码器,4K超清HDMI编解码器,双路4K超高清编解码器 同三维T80004EA编解码器视频使用操作说明书:高清HDMI编解码器,高清SDI编解码器&am…...

ChatGPT提问获取高质量答案的艺术PDF下载书籍推荐分享
ChatGPT高质量prompt技巧分享pdf, ChatGPT提问获取高质量答案的艺术pdf。本书是一本全面的指南,介绍了各种 Prompt 技术的理解和利用,用于从 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搜索不到任何文件-设置
版本: V1.4.1.1024 (x64) 问题:搜索不到任何文件 click:[工具]->[选项]->下图所示 将本地磁盘都选中包含...

python如何结束程序运行
方法1:采用sys.exit(0),正常终止程序,从图中可以看到,程序终止后shell运行不受影响。 方法2:采用os._exit(0)关闭整个shell,从图中看到,调用sys._exit(0)后整个shell都重启了(RESTAR…...
InnoDB
InnoDB 是 MySQL 默认的存储引擎,它提供了事务支持、行级锁定和外键约束等高级功能。下面详细解析 InnoDB 的一些底层原理和关键特性。 1. 数据存储结构 表空间(Tablespace) InnoDB 使用表空间来管理数据存储,表空间可以是共享…...
spark运行报错:Container killed by YARN for exceeding memory limits
用spark跑数据量大的离线调度任务报错: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
一.模型 模型,简单来说,就是用来表示或解释某个事物、现象或系统的一种工具或框架。它可以是实体的,也可以是虚拟的,目的是为了帮助我们更好地理解和预测所描述的对象。在生活中,模型无处不在,它们以各种形…...
数学基础 -- 三角学
三角学 三角学(Trigonometry)是数学的一个分支,主要研究三角形的边长与角度之间的关系。三角学在几何学、物理学、工程学等多个领域中有广泛的应用。以下是三角学的一些基本概念和公式: 基本概念 直角三角形:一个角…...
基于BitMap的工作日间隔计算
背景问题 在我们实际开发过程中,时常会遇到日期的间隔计算,即计算多少工作日之后的日期,在不考虑法定节假日的情况下也不是那么复杂,毕竟周六、周日是相对固定的,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/ 源代码位置:Lib/sqlite3/ SQLite is a C…...
Spring Boot中的安全配置与实现
Spring Boot中的安全配置与实现 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将深入探讨Spring Boot中的安全配置与实现,看看如何保护你的…...

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):实现一个Token过滤器配置,用于拦截HTTP请求,从请求头中提取Token&…...

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

19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...

高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
鸿蒙(HarmonyOS5)实现跳一跳小游戏
下面我将介绍如何使用鸿蒙的ArkUI框架,实现一个简单的跳一跳小游戏。 1. 项目结构 src/main/ets/ ├── MainAbility │ ├── pages │ │ ├── Index.ets // 主页面 │ │ └── GamePage.ets // 游戏页面 │ └── model │ …...

汇编语言学习(三)——DoxBox中debug的使用
目录 一、安装DoxBox,并下载汇编工具(MASM文件) 二、debug是什么 三、debug中的命令 一、安装DoxBox,并下载汇编工具(MASM文件) 链接: https://pan.baidu.com/s/1IbyJj-JIkl_oMOJmkKiaGQ?pw…...

Redis专题-实战篇一-基于Session和Redis实现登录业务
GitHub项目地址:https://github.com/whltaoin/redisLearningProject_hm-dianping 基于Session实现登录业务功能提交版本码:e34399f 基于Redis实现登录业务提交版本码:60bf740 一、导入黑马点评后端项目 项目架构图 1. 前期阶段2. 后续阶段导…...
CMake系统学习笔记
CMake系统学习笔记 基础操作 最基本的案例 // code #include <iostream>int main() {std::cout << "hello world " << std::endl;return 0; }// CMakeLists.txt cmake_minimum_required(VERSION 3.0)# 定义当前工程名称 project(demo)add_execu…...

NLP学习路线图(三十四): 命名实体识别(NER)
一、命名实体识别(NER)是什么? 命名实体识别(Named Entity Recognition, NER)是自然语言处理中的一项关键序列标注任务。其核心目标是从非结构化的文本中自动识别出特定类别的名词性短语,并将其归类到预定义的类别中。 核心目标:找到文本中提到的命名实体,并分类。 典…...