diff --git a/modules/auth/wechat/access_token.go b/modules/auth/wechat/access_token.go new file mode 100644 index 000000000..a312db215 --- /dev/null +++ b/modules/auth/wechat/access_token.go @@ -0,0 +1,44 @@ +package wechat + +import ( + "code.gitea.io/gitea/modules/redis/redis_client" + "code.gitea.io/gitea/modules/redis/redis_key" + "time" +) + +const EMPTY_REDIS_VAL = "Nil" + +func GetWechatAccessToken() string { + token, _ := redis_client.Get(redis_key.WechatAccessTokenKey()) + if token != "" { + if token == EMPTY_REDIS_VAL { + return "" + } + live, _ := redis_client.TTL(token) + //refresh wechat access token when expire time less than 5 minutes + if live > 0 && live < 300 { + refreshAccessTokenCache() + } + return token + } + return refreshAccessTokenCache() +} + +func refreshAccessTokenCache() string { + r := callAccessToken() + + var token string + if r != nil { + token = r.Access_token + } + + if token == "" { + redis_client.Setex(redis_key.WechatAccessTokenKey(), EMPTY_REDIS_VAL, 10*time.Second) + return "" + } + + redis_client.Setex(redis_key.WechatAccessTokenKey(), token, time.Duration(r.Expires_in)*time.Second) + + return token + +} diff --git a/modules/auth/wechat/client.go b/modules/auth/wechat/client.go new file mode 100644 index 000000000..edf010b33 --- /dev/null +++ b/modules/auth/wechat/client.go @@ -0,0 +1,95 @@ +package wechat + +import ( + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "encoding/json" + "github.com/go-resty/resty/v2" + "time" +) + +var ( + client *resty.Client +) + +const ( + GRANT_TYPE = "client_credential" + ACCESS_TOKEN_PATH = "/cgi-bin/token" + QR_CODE_Path = "/cgi-bin/qrcode/create" + ACTION_QR_STR_SCENE = "QR_STR_SCENE" +) + +type AccessTokenResponse struct { + Access_token string + Expires_in int +} + +type QRCodeResponse struct { + Ticket string `json:"ticket"` + Expire_Seconds int `json:"expire_seconds"` + Url string `json:"url"` +} + +type QRCodeRequest struct { + Action_name string `json:"action_name"` + Action_info ActionInfo `json:"action_info"` + Expire_seconds int `json:"expire_seconds"` +} + +type ActionInfo struct { + Scene Scene +} + +type Scene struct { + Scene_str string +} + +func getWechatRestyClient() *resty.Client { + if client == nil { + client = resty.New() + client.SetTimeout(time.Duration(setting.WechatApiTimeoutSeconds) * time.Second) + } + return client +} + +func callAccessToken() *AccessTokenResponse { + client := getWechatRestyClient() + + var result AccessTokenResponse + _, err := client.R(). + SetQueryParam("grant_type", GRANT_TYPE). + SetQueryParam("appid", setting.WechatAppId). + SetQueryParam("secret", setting.WechatAppSecret). + SetResult(&result). + Get(setting.WechatApiHost + ACCESS_TOKEN_PATH) + if err != nil { + log.Error("get wechat access token failed,e=%v", err) + return nil + } + return &result +} + +func callQRCodeCreate(sceneStr string) *QRCodeResponse { + client := getWechatRestyClient() + + body := &QRCodeRequest{ + Action_name: ACTION_QR_STR_SCENE, + Action_info: ActionInfo{Scene: Scene{Scene_str: sceneStr}}, + Expire_seconds: setting.WechatQRCodeExpireSeconds, + } + bodyJson, _ := json.Marshal(body) + var result QRCodeResponse + r, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetQueryParam("access_token", GetWechatAccessToken()). + SetBody(bodyJson). + SetResult(&result). + Post(setting.WechatApiHost + QR_CODE_Path) + if err != nil || result.Url == "" { + log.Error("create QR code failed,e=%v", err) + return nil + } + //todo 识别token失效的错误码,重试机制 + log.Info("%v", r) + return &result +} diff --git a/modules/auth/wechat/event.go b/modules/auth/wechat/event.go new file mode 100644 index 000000000..ecd4201a5 --- /dev/null +++ b/modules/auth/wechat/event.go @@ -0,0 +1 @@ +package wechat diff --git a/modules/auth/wechat/qr_code.go b/modules/auth/wechat/qr_code.go new file mode 100644 index 000000000..1e462861e --- /dev/null +++ b/modules/auth/wechat/qr_code.go @@ -0,0 +1,6 @@ +package wechat + +func GetWechatQRCode4Bind(sceneStr string) *QRCodeResponse { + r := callQRCodeCreate(sceneStr) + return r +} diff --git a/modules/redis/redis_client/client.go b/modules/redis/redis_client/client.go new file mode 100644 index 000000000..0590f13b1 --- /dev/null +++ b/modules/redis/redis_client/client.go @@ -0,0 +1,54 @@ +package redis_client + +import ( + "code.gitea.io/gitea/modules/labelmsg" + "fmt" + "math" + "strconv" + "time" +) + +//todo redis连接池 +func Setex(key, value string, timeout time.Duration) (bool, error) { + redisClient := labelmsg.Get() + defer redisClient.Close() + + seconds := int(math.Floor(timeout.Seconds())) + reply, err := redisClient.Do("SETEX", key, seconds, value) + if err != nil { + return false, err + } + if reply != "OK" { + return false, nil + } + return true, nil + +} + +func Get(key string) (string, error) { + redisClient := labelmsg.Get() + defer redisClient.Close() + + reply, err := redisClient.Do("GET", key) + if err != nil { + return "", err + } + if reply == nil { + return "", err + } + return fmt.Sprint(reply), nil + +} + +func TTL(key string) (int, error) { + redisClient := labelmsg.Get() + defer redisClient.Close() + + reply, err := redisClient.Do("TTL", key) + if err != nil { + return 0, err + } + n, _ := strconv.Atoi(fmt.Sprint(reply)) + return n, nil + +} diff --git a/modules/redis/redis_key/key_base.go b/modules/redis/redis_key/key_base.go new file mode 100644 index 000000000..0efc6ed38 --- /dev/null +++ b/modules/redis/redis_key/key_base.go @@ -0,0 +1,16 @@ +package redis_key + +import "strings" + +const KEY_SEPARATE = ":" + +func KeyJoin(keys ...string) string { + var build strings.Builder + for _, v := range keys { + build.WriteString(v) + build.WriteString(KEY_SEPARATE) + } + s := build.String() + s = strings.TrimSuffix(s, KEY_SEPARATE) + return s +} diff --git a/modules/redis/redis_key/wechat_redis_key.go b/modules/redis/redis_key/wechat_redis_key.go new file mode 100644 index 000000000..def7e93ee --- /dev/null +++ b/modules/redis/redis_key/wechat_redis_key.go @@ -0,0 +1,11 @@ +package redis_key + +const PREFIX = "wechat" + +func WechatBindingUserIdKey(sceneStr string) string { + return KeyJoin(PREFIX, sceneStr, "scene_userId") +} + +func WechatAccessTokenKey() string { + return KeyJoin(PREFIX, "access_token") +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index c6828f9f7..227a05656 100755 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -470,7 +470,7 @@ var ( BenchmarkTypes string BenchmarkGpuTypes string BenchmarkResourceSpecs string - BenchmarkMaxDuration int64 + BenchmarkMaxDuration int64 //snn4imagenet config IsSnn4imagenetEnabled bool @@ -512,7 +512,7 @@ var ( ProfileID string PoolInfos string Flavor string - DebugHost string + DebugHost string //train-job ResourcePools string Engines string @@ -529,6 +529,13 @@ var ( ElkTimeFormat string PROJECT_LIMIT_PAGES []string + //wechat config + WechatApiHost string + WechatApiTimeoutSeconds int + WechatAppId string + WechatAppSecret string + WechatQRCodeExpireSeconds int + //nginx proxy PROXYURL string RadarMap = struct { @@ -1342,6 +1349,13 @@ func NewContext() { ElkTimeFormat = sec.Key("ELKTIMEFORMAT").MustString("date_time") PROJECT_LIMIT_PAGES = strings.Split(sec.Key("project_limit_pages").MustString(""), ",") + sec = Cfg.Section("wechat") + WechatApiHost = sec.Key("HOST").MustString("https://api.weixin.qq.com") + WechatApiTimeoutSeconds = sec.Key("TIMEOUT_SECONDS").MustInt(3) + WechatAppId = sec.Key("APP_ID").MustString("wxba77b915a305a57d") + WechatAppSecret = sec.Key("APP_SECRET").MustString("e48e13f315adc32749ddc7057585f198") + WechatQRCodeExpireSeconds = sec.Key("QR_CODE_EXPIRE_SECONDS").MustInt(120) + SetRadarMapConfig() sec = Cfg.Section("warn_mail") diff --git a/routers/authentication/wechat.go b/routers/authentication/wechat.go new file mode 100644 index 000000000..d9004ace9 --- /dev/null +++ b/routers/authentication/wechat.go @@ -0,0 +1,67 @@ +package authentication + +import ( + "code.gitea.io/gitea/modules/auth/wechat" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/redis/redis_client" + "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/modules/setting" + "errors" + "fmt" + gouuid "github.com/satori/go.uuid" + "time" +) + +type QRCodeResponse struct { + Url string + Ticket string + SceneStr string + ExpireSeconds int +} + +// GetQRCode4Bind get QR code for wechat binding +func GetQRCode4Bind(ctx *context.Context) { + userId := ctx.User.ID + + r, err := createQRCode4Bind(userId) + if err != nil { + ctx.JSON(200, map[string]interface{}{ + "code": 9, + "msg": "Get QR code failed", + }) + return + } + + ctx.JSON(200, map[string]interface{}{ + "code": 0, + "msg": "success", + "data": r, + }) +} + +func createQRCode4Bind(userId int64) (*QRCodeResponse, error) { + sceneStr := gouuid.NewV4().String() + r := wechat.GetWechatQRCode4Bind(sceneStr) + if r == nil { + return nil, errors.New("createQRCode4Bind failed") + } + + isOk, err := redis_client.Setex(redis_key.WechatBindingUserIdKey(sceneStr), fmt.Sprint(userId), time.Duration(setting.WechatQRCodeExpireSeconds)*time.Second) + if err != nil { + log.Error("createQRCode4Bind failed.e=%v", err) + return nil, err + } + if !isOk { + log.Error("createQRCode4Bind failed.redis reply is not ok") + return nil, errors.New("reply is not ok when set WechatBindingUserIdKey") + } + + result := &QRCodeResponse{ + Url: r.Url, + Ticket: r.Ticket, + SceneStr: sceneStr, + ExpireSeconds: setting.WechatQRCodeExpireSeconds, + } + return result, nil +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index aa85b8f2b..9613b43cd 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -6,6 +6,7 @@ package routes import ( "bytes" + "code.gitea.io/gitea/routers/authentication" "encoding/gob" "net/http" "path" @@ -391,6 +392,13 @@ func RegisterRoutes(m *macaron.Macaron) { }, ignSignInAndCsrf, reqSignIn) m.Post("/login/oauth/access_token", bindIgnErr(auth.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) + m.Group("/authentication/wechat", func() { + m.Get("/qrCode4Bind", authentication.GetQRCode4Bind) + m.Post("/grant", bindIgnErr(auth.GrantApplicationForm{}), user.GrantApplicationOAuth) + // TODO manage redirection + m.Post("/authorize", bindIgnErr(auth.AuthorizationForm{}), user.AuthorizeOAuth) + }, reqSignIn) + m.Group("/user/settings", func() { m.Get("", userSetting.Profile) m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost)