From 4605ba79d53089db2b9a07596eb0cedde54bdaf2 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 5 Nov 2022 13:14:28 +0800 Subject: [PATCH] Token refresh & related API is done --- README.md | 37 +++++++++++--------- common/access-token-store.go | 68 ++++++++++++++++++++++++++++++++++++ common/logger.go | 5 +++ common/utils.go | 2 +- controller/misc.go | 10 ++++++ main.go | 3 ++ router/api-router.go | 1 + 7 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 common/access-token-store.go diff --git a/README.md b/README.md index b90a1b0..c5de401 100644 --- a/README.md +++ b/README.md @@ -2,31 +2,34 @@ > 用以微信公众号的后端,提供登录验证功能 ## 功能 -+ [ ] 登录验证 ++ [x] Access Token 自动刷新 & 提供外部访问接口 + [ ] 自定义菜单 ++ [ ] 登录验证 + [ ] 自定义回复 -+ [ ] Access Token 中控服务器 -## 用法 +## 配置 1. 从 [GitHub Releases](https://github.com/songquanpeng/wechat-server/releases/latest) 下载可执行文件。 -2. 运行: +2. 系统本身开箱即用,有一些环境变量可供配置: + 1. `REDIS_CONN_STRING`: 设置之后,将启用 Redis。 + + 例如:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153` + 2. `SESSION_SECRET`:设置之后,将使用给定会话密钥。 + + 例如:`SESSION_SECRET=random_string` + 3. `SQL_DSN`: 设置之后,将使用目标数据库而非 SQLite。 + + 例如:`SQL_DSN=root:123456@tcp(localhost:3306)/gofile` +3. 运行: 1. `chmod u+x wechat-server` 2. `./wechat-server --port 3000` -3. 初始账户用户名为 `root`,密码为 `123456`,记得登录后立刻修改密码。 -4. 前往[微信公众号配置页面 -> 设置与开发 -> 基本配置](https://mp.weixin.qq.com/)获取 AppID 和 AppSecret,并在我们的配置页面填入上述信息,另外还需要配置 IP 白名单,按照页面上的提示完成即可。 -5. 前往[微信公众号配置页面 -> 设置与开发 -> 基本配置](https://mp.weixin.qq.com/)填写以下配置: +4. 初始账户用户名为 `root`,密码为 `123456`,记得登录后立刻修改密码。 +5. 前往[微信公众号配置页面 -> 设置与开发 -> 基本配置](https://mp.weixin.qq.com/)获取 AppID 和 AppSecret,并在我们的配置页面填入上述信息,另外还需要配置 IP 白名单,按照页面上的提示完成即可。 +6. 前往[微信公众号配置页面 -> 设置与开发 -> 基本配置](https://mp.weixin.qq.com/)填写以下配置: 1. `URL` 填:`https:///api/wechat_verification` 2. `Token` 首先在我们的配置页面随便填写一个 Token,然后在微信公众号的配置页面填入同一个 Token 即可。 3. `EncodingAESKey` 点随机生成,然后在我们的配置页面填入该值。 4. 消息加解密方式选择明文模式。 +7. 之后保存设置并启用设置。 -## 配置 -系统本身开箱即用。 - -有一些环境遍历可供配置: -1. `REDIS_CONN_STRING`: 设置之后,将启用 Redis。 - + 例如:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153` -2. `SESSION_SECRET`:设置之后,将使用给定会话密钥。 - + 例如:`SESSION_SECRET=random_string` -3. `SQL_DSN`: 设置之后,将使用目标数据库而非 SQLite。 - + 例如:`SQL_DSN=root:123456@tcp(localhost:3306)/gofile` \ No newline at end of file +## API +### 获取 Access Token +1. 请求方法:`GET` +2. URL:`/api/access_token` +3. 无参数,但是需要设置 HTTP 头部:`Authorization: your token` \ No newline at end of file diff --git a/common/access-token-store.go b/common/access-token-store.go new file mode 100644 index 0000000..ba01f95 --- /dev/null +++ b/common/access-token-store.go @@ -0,0 +1,68 @@ +package common + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" + "time" +) + +type accessTokenStore struct { + AccessToken string + Mutex sync.RWMutex + ExpirationSeconds int +} + +type response struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` +} + +var s accessTokenStore + +func InitAccessTokenStore() { + go func() { + for { + RefreshAccessToken() + s.Mutex.RLock() + sleepDuration := Max(s.ExpirationSeconds, 60) + s.Mutex.RUnlock() + time.Sleep(time.Duration(sleepDuration) * time.Second) + } + }() +} + +func RefreshAccessToken() { + // https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html + client := http.Client{ + Timeout: 5 * time.Second, + } + req, err := http.NewRequest("GET", fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", WeChatAppID, WeChatAppSecret), nil) + if err != nil { + SysError(err.Error()) + return + } + responseData, err := client.Do(req) + if err != nil { + SysError("failed to refresh access token: " + err.Error()) + return + } + var res response + err = json.NewDecoder(responseData.Body).Decode(&res) + if err != nil { + SysError("failed to decode response: " + err.Error()) + return + } + s.Mutex.Lock() + s.AccessToken = res.AccessToken + s.ExpirationSeconds = res.ExpiresIn + s.Mutex.Unlock() + SysLog("access token refreshed") +} + +func GetAccessToken() (string, int) { + s.Mutex.RLock() + defer s.Mutex.RUnlock() + return s.AccessToken, s.ExpirationSeconds +} diff --git a/common/logger.go b/common/logger.go index 8073036..0b8b2cf 100644 --- a/common/logger.go +++ b/common/logger.go @@ -32,6 +32,11 @@ func SysLog(s string) { _, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) } +func SysError(s string) { + t := time.Now() + _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) +} + func FatalLog(v ...any) { t := time.Now() _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v) diff --git a/common/utils.go b/common/utils.go index 339ca94..566c607 100644 --- a/common/utils.go +++ b/common/utils.go @@ -117,7 +117,7 @@ func UnescapeHTML(x string) interface{} { return template.HTML(x) } -func IntMax(a int, b int) int { +func Max(a int, b int) int { if a >= b { return a } else { diff --git a/controller/misc.go b/controller/misc.go index d60133d..fb7c5ed 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -58,6 +58,16 @@ func WeChatVerification(c *gin.Context) { } } +func GetAccessToken(c *gin.Context) { + accessToken, expiration := common.GetAccessToken() + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "access_token": accessToken, + "expiration": expiration, + }) +} + func SendEmailVerification(c *gin.Context) { email := c.Query("email") if err := common.Validate.Var(email, "required,email"); err != nil { diff --git a/main.go b/main.go index cf99f08..4e23156 100644 --- a/main.go +++ b/main.go @@ -48,6 +48,9 @@ func main() { // Initialize options model.InitOptionMap() + // Initialize access token store + common.InitAccessTokenStore() + // Initialize HTTP server server := gin.Default() server.Use(middleware.CORS()) diff --git a/router/api-router.go b/router/api-router.go index 2de5377..81c3c30 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -17,6 +17,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), controller.SendPasswordResetEmail) apiRouter.GET("/user/reset", controller.SendNewPasswordEmail) apiRouter.GET("/oauth/github", controller.GitHubOAuth) + apiRouter.GET("/access_token", middleware.AdminAuth(), controller.GetAccessToken) userRoute := apiRouter.Group("/user") {