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。
<?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授权设计实践的更多相关文章
- [系列] Gin框架 - 数据绑定和验证
目录 概述 推荐阅读 概述 上篇文章分享了 Gin 框架使用 Logrus 进行日志记录,这篇文章分享 Gin 框架的数据绑定与验证. 有读者咨询我一个问题,如何让框架的运行日志不输出控制台? 解决方 ...
- gin框架教程:代码系列demo地址
gin框架教程代码地址: https://github.com/jiujuan/gin-tutorial demo目录: 01quickstart 02parameter 03route 04midd ...
- Gin框架系列02:路由与参数
回顾 上一节我们用Gin框架快速搭建了一个GET请求的接口,今天来学习路由和参数的获取. 请求动词 熟悉RESTful的同学应该知道,RESTful是网络应用程序的一种设计风格和开发方式,每一个URI ...
- Gin框架系列03:换个姿势理解中间件
什么是中间件 中间件,英译middleware,顾名思义,放在中间的物件,那么放在谁中间呢?本来,客户端可以直接请求到服务端接口. 现在,中间件横插一脚,它能在请求到达接口之前拦截请求,做一些特殊处理 ...
- gin框架使用注意事项
gin框架使用注意事项 本文就说下这段时间我在使用gin框架过程中遇到的问题和要注意的事情. 错误处理请求返回要使用c.Abort,不要只是return 当在controller中进行错误处理的时候, ...
- Gin框架 - 自定义错误处理
目录 概述 错误处理 自定义错误处理 panic 和 recover 推荐阅读 概述 很多读者在后台向我要 Gin 框架实战系列的 Demo 源码,在这里再说明一下,源码我都更新到 GitHub 上, ...
- Thinkphp入门三—框架模板、变量(47)
原文:Thinkphp入门三-框架模板.变量(47) [在控制器调用模板] display() 调用当前操作名称的模板 display(‘名字’) 调用指定名字的模板文件 控制器调用模板四种方式 ...
- 基于gin框架和jwt-go中间件实现小程序用户登陆和token验证
本文核心内容是利用jwt-go中间件来开发golang webapi用户登陆模块的token下发和验证,小程序登陆功能只是一个切入点,这套逻辑同样适用于其他客户端的登陆处理. 小程序登陆逻辑 小程序的 ...
- Gin框架源码解析
Gin框架源码解析 Gin框架是golang的一个常用的web框架,最近一个项目中需要使用到它,所以对这个框架进行了学习.gin包非常短小精悍,不过主要包含的路由,中间件,日志都有了.我们可以追着代码 ...
- gin框架学习手册
前言 gin框架是go语言的一个框架,框架的github地址是:https://github.com/gin-gonic/gin 转载本文,请标注原文地址:https://www.cnblogs.co ...
随机推荐
- 1 - 香橙派硬件PWM控制sg90舵机
本人机械电子专业的大一学生一枚,这是我在博客园的第一篇随笔 2024年4月份我在二手平台花费300大洋入手了香橙派zero3和3B,买回来后一开始是装上ubuntu跑QQ机器人和minecraft ...
- .NET8 Identity Register
分享给需要帮助的人:记一次 IdentityAPI 中注册的源码解读:设置用户账户为未验证状态,以及除此之外更安全的做法: 延迟用户创建.包含了对优缺点的说明,以及适用场景. 在ASP.NET 8 I ...
- Istio(六):Istio弹性(超时&重试)和故障注入
目录 一.模块概览 二.系统环境 三.弹性(超时&重试) 3.1 弹性 四.故障注入 4.1 故障注入 五.实战:观察错误注入 5.1 在 Grafana.Zipkin 和 Kiali 中观察 ...
- vue2遇到的一些错误
一.VUE中的VUEX如何调用modules里面的mutations和state ...mapMutations("workflow",['setApproverConfig' ...
- Flask源码阅读
上下文篇 整个Flask生命周期中都依赖LocalStack()栈?.而LocalStack()分为请求上下文_request_ctx_stack和应用上下文_app_ctx_stack. _requ ...
- Sed 日常使用介绍
Sed 日常使用介绍 简介 sed 是 unix 环境下常用的流处理工具, 可以处理字符流, 文件或者二进制文件流. 各个 unix/linux 发行版都会配备 sed 及其衍生的命令工具, 因此, ...
- mysql笔记第一天: 介绍和MySQL编译安装
一.DBA的工作内容: ![](371eaced-e10b-46d9-89e2-f63f15503bb6_files/9edcd22a-ef2d-4c3e-8474-3049255610db.jpg) ...
- Linux进程间通信-FIFO(命名管道)
本系列文章主要是学习记录Linux下进程间通信的方式. 常用的进程间通信方式:管道.FIFO.消息队列.信号量以及共享存储. 参考文档:<UNIX环境高级编程(第三版)> 参考视频:Lin ...
- Wakeup Source框架设计与实现
Wakeup Source 为系统组件提供了投票机制,以便低功耗子系统判断当前是否可以进入休眠. Wakeup Source(后简称:WS) 模块可与内核中的其他模块或者上层服务交互,并最终体现在对睡 ...
- 算法学习笔记(15): Trie(字典树)
Trie树 Trie(字典树)是一种用于实现字符串检索的多叉树. Trie的每一个节点都可以通过 c 转移到下一层的一个节点. 我们可以看作可以通过某个字符转移到下一个字符串状态,直到转移到最终态为止 ...