[DIP-1] 微信公众号二维码登录:本地部署与线上部署双端共用代码
All checks were successful
DevStar Studio CI Pipeline - dev branch / build-and-push-x86-64-docker-image (push) Successful in 8m14s

* 恢复误删了的从未登录状态下的页面登录后跳转到当前页的功能
* secret是敏感信息,不能打印在日志里面
* fixed bug: 根据配置AppID和AppSecret来createPowerWechatApp
* 完成本地部署微信二维码登录功能,用户设置绑定微信的功能本地测试正常
* 本地部署可以扫码跳转注册页面,尚未查询用户数据
* 本地部署的后端已经可以和devstar.cn上的微信代理API打通,但是还没有调用本地用户认证相关代码,功能上还不完整
* 优化了signin navbar前端显示逻辑,根据app.ini配置使能wechat qr和openid
* 增加wechat配置项,以便同时支持直接和间接的微信二维码登录
* 恢复openid原有的初始配置方法
* 默认支持微信二维码登录(仅在安装配置上实现,功能上尚未实现),默认disabled openid
* "Initial commit from " + gitURL + " ( " + sha1 + " ) "
This commit is contained in:
孟宁
2024-12-10 08:41:20 +00:00
committed by 戴明辰
parent 4a55a192bb
commit 3edc4ce1e4
16 changed files with 297 additions and 119 deletions

View File

@@ -24,6 +24,7 @@ type PowerWechatOfficialAccountUserConfigType struct {
type OfficialAccountType struct { type OfficialAccountType struct {
Enabled bool Enabled bool
DefaultDomainName string
UserConfig PowerWechatOfficialAccountUserConfigType UserConfig PowerWechatOfficialAccountUserConfigType
TempQrExpireSeconds int TempQrExpireSeconds int
PowerWechat *PowerWechatOfficialAccountType.OfficialAccount PowerWechat *PowerWechatOfficialAccountType.OfficialAccount
@@ -40,12 +41,20 @@ type OfficialAccountType struct {
*/ */
func loadWechatSettingsFrom(rootCfg ConfigProvider) { func loadWechatSettingsFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("wechat") sec := rootCfg.Section("wechat")
Wechat.OfficialAccount.Enabled = sec.Key("ENABLED_WECHAT_QR_SIGNIN").MustBool(true)
log.Info("ENABLED_WECHAT_QR_SIGNIN == '%b'", Wechat.OfficialAccount.Enabled)
Wechat.OfficialAccount.DefaultDomainName = sec.Key("WECHAT_QR_SERVICE_DOMAIN_NAME").MustString("devstar.cn")
Wechat.OfficialAccount.UserConfig.AppID = sec.Key("WECHAT_OFFICIAL_ACCOUNT_APP_ID").MustString("") Wechat.OfficialAccount.UserConfig.AppID = sec.Key("WECHAT_OFFICIAL_ACCOUNT_APP_ID").MustString("")
Wechat.OfficialAccount.UserConfig.AppSecret = sec.Key("WECHAT_OFFICIAL_ACCOUNT_APP_SECRET").MustString("") Wechat.OfficialAccount.UserConfig.AppSecret = sec.Key("WECHAT_OFFICIAL_ACCOUNT_APP_SECRET").MustString("")
Wechat.OfficialAccount.UserConfig.RedisAddr = sec.Key("WECHAT_OFFICIAL_ACCOUNT_REDIS_ADDR").MustString("") Wechat.OfficialAccount.UserConfig.RedisAddr = sec.Key("WECHAT_OFFICIAL_ACCOUNT_REDIS_ADDR").MustString("")
Wechat.OfficialAccount.UserConfig.MessageToken = sec.Key("WECHAT_OFFICIAL_ACCOUNT_MESSAGE_TOKEN").MustString("") Wechat.OfficialAccount.UserConfig.MessageToken = sec.Key("WECHAT_OFFICIAL_ACCOUNT_MESSAGE_TOKEN").MustString("")
Wechat.OfficialAccount.UserConfig.MessageAesKey = sec.Key("WECHAT_OFFICIAL_ACCOUNT_MESSAGE_AES_KEY").MustString("") Wechat.OfficialAccount.UserConfig.MessageAesKey = sec.Key("WECHAT_OFFICIAL_ACCOUNT_MESSAGE_AES_KEY").MustString("")
createPowerWechatApp(Wechat.OfficialAccount.UserConfig) if Wechat.OfficialAccount.UserConfig.AppID != "" && Wechat.OfficialAccount.UserConfig.AppSecret != "" {
log.Info("createPowerWechatApp AppID:%s ", Wechat.OfficialAccount.UserConfig.AppID)
createPowerWechatApp(Wechat.OfficialAccount.UserConfig)
}
Wechat.OfficialAccount.TempQrExpireSeconds = sec.Key("WECHAT_OFFICIAL_ACCOUNT_TEMP_QR_EXPIRE_SECONDS").MustInt(60) Wechat.OfficialAccount.TempQrExpireSeconds = sec.Key("WECHAT_OFFICIAL_ACCOUNT_TEMP_QR_EXPIRE_SECONDS").MustInt(60)
// 扫码后最长注册时间默认24小时 // 扫码后最长注册时间默认24小时
Wechat.OfficialAccount.RegisterationExpireSeconds = sec.Key("WECHAT_OFFICIAL_ACCOUNT_REGISTERATION_EXPIRE_SECONDS").MustInt(86400) Wechat.OfficialAccount.RegisterationExpireSeconds = sec.Key("WECHAT_OFFICIAL_ACCOUNT_REGISTERATION_EXPIRE_SECONDS").MustInt(86400)
@@ -87,8 +96,5 @@ func createPowerWechatApp(userConfig PowerWechatOfficialAccountUserConfigType) {
}) })
if err != nil { if err != nil {
log.Warn("创建微信工具类 PowerWechat 失败,请检查 modules/setting/wechat.go ") log.Warn("创建微信工具类 PowerWechat 失败,请检查 modules/setting/wechat.go ")
} else {
Wechat.OfficialAccount.Enabled = true
} }
} }

View File

@@ -307,6 +307,8 @@ federated_avatar_lookup_popup = Enable federated avatar lookup using Libravatar.
disable_registration = Disable Self-Registration disable_registration = Disable Self-Registration
disable_registration_popup = Disable user self-registration. Only administrators will be able to create new user accounts. disable_registration_popup = Disable user self-registration. Only administrators will be able to create new user accounts.
allow_only_external_registration_popup = Allow Registration Only Through External Services allow_only_external_registration_popup = Allow Registration Only Through External Services
wechat_qr_signin = Enable Wechat QR Code Sign-In
wechat_qr_signin_popup = Enable user sign-in via Wechat QR Code.
openid_signin = Enable OpenID Sign-In openid_signin = Enable OpenID Sign-In
openid_signin_popup = Enable user sign-in via OpenID. openid_signin_popup = Enable user sign-in via OpenID.
openid_signup = Enable OpenID Self-Registration openid_signup = Enable OpenID Self-Registration

View File

@@ -306,6 +306,8 @@ federated_avatar_lookup_popup=启用 Federated Avatars 查找以使用开源的
disable_registration=禁止用户自助注册 disable_registration=禁止用户自助注册
disable_registration_popup=禁用用户自助注册。只有管理员才能创建新的用户帐户。 disable_registration_popup=禁用用户自助注册。只有管理员才能创建新的用户帐户。
allow_only_external_registration_popup=仅允许通过外部服务注册 allow_only_external_registration_popup=仅允许通过外部服务注册
wechat_qr_signin=启用 微信二维码 登录
wechat_qr_signin_popup=启用通过 微信二维码 登录
openid_signin=启用 OpenID 登录 openid_signin=启用 OpenID 登录
openid_signin_popup=启用通过 OpenID 登录 openid_signin_popup=启用通过 OpenID 登录
openid_signup=启用 OpenID 自助注册 openid_signup=启用 OpenID 自助注册

View File

@@ -1,9 +1,10 @@
package official_account package official_account
import ( import (
"net/http"
Result "code.gitea.io/gitea/routers/entity" Result "code.gitea.io/gitea/routers/entity"
wechat_official_account_service "code.gitea.io/gitea/services/wechat/official_account" wechat_official_account_service "code.gitea.io/gitea/services/wechat/official_account"
"net/http"
) )
// QrCheckCodeStatus 检查二维码扫描状态 // QrCheckCodeStatus 检查二维码扫描状态
@@ -31,7 +32,6 @@ func QrCheckCodeStatus(responseWriter http.ResponseWriter, request *http.Request
Result.RespFailedIllegalWechatQrTicket.RespondJson2HttpResponseWriter(responseWriter) Result.RespFailedIllegalWechatQrTicket.RespondJson2HttpResponseWriter(responseWriter)
return return
} }
// 调用 WeChat Official Account Service 层,获取二维码扫码状态 // 调用 WeChat Official Account Service 层,获取二维码扫码状态
qrStatusVO, err := wechat_official_account_service.GetTempQRStatus(ticket) qrStatusVO, err := wechat_official_account_service.GetTempQRStatus(ticket)
if err != nil { if err != nil {

View File

@@ -153,6 +153,7 @@ func Install(ctx *context.Context) {
form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
form.EnableWechatQRSignIn = true
form.DisableRegistration = setting.Service.DisableRegistration form.DisableRegistration = setting.Service.DisableRegistration
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
form.EnableCaptcha = setting.Service.EnableCaptcha form.EnableCaptcha = setting.Service.EnableCaptcha
@@ -445,7 +446,17 @@ func SubmitInstall(ctx *context.Context) {
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return return
} }
if form.EnableWechatQRSignIn {
cfg.Section("wechat").Key("ENABLED_WECHAT_QR_SIGNIN").SetValue("true")
// cfg.Section("wechat").Key("WECHAT_QR_SERVICE_DOMAIN_NAME").SetValue("devstar.cn")
// cfg.Section("wechat").Key("WECHAT_OFFICIAL_ACCOUNT_TEMP_QR_EXPIRE_SECONDS").SetValue("180")
// cfg.Section("wechat").Key("WECHAT_OFFICIAL_ACCOUNT_REGISTERATION_EXPIRE_SECONDS").SetValue("86400")
// cfg.Section("wechat").Key("WECHAT_OFFICIAL_ACCOUNT_APP_ID").SetValue("")
// cfg.Section("wechat").Key("WECHAT_OFFICIAL_ACCOUNT_APP_SECRET").SetValue("")
// cfg.Section("wechat").Key("WECHAT_OFFICIAL_ACCOUNT_REDIS_ADDR").SetValue("")
// cfg.Section("wechat").Key("WECHAT_OFFICIAL_ACCOUNT_MESSAGE_TOKEN").SetValue("")
// cfg.Section("wechat").Key("WECHAT_OFFICIAL_ACCOUNT_MESSAGE_AES_KEY").SetValue("")
}
cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn))
cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp))
cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration)) cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration))

View File

@@ -5,9 +5,6 @@
package auth package auth
import ( import (
wechat_official_account_openid_model "code.gitea.io/gitea/models/wechat"
wechat_official_account_cache_service "code.gitea.io/gitea/services/wechat/cache"
wechat_official_account_service "code.gitea.io/gitea/services/wechat/official_account"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
@@ -15,6 +12,10 @@ import (
"net/url" "net/url"
"strings" "strings"
wechat_official_account_openid_model "code.gitea.io/gitea/models/wechat"
wechat_official_account_cache_service "code.gitea.io/gitea/services/wechat/cache"
wechat_official_account_service "code.gitea.io/gitea/services/wechat/official_account"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@@ -167,6 +168,15 @@ func CheckAutoLogin(ctx *context.Context) bool {
return false return false
} }
func SignInNavbarSetting(ctx *context.Context) {
ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn
ctx.Data["EnableWechatQrLogin"] = setting.Wechat.OfficialAccount.Enabled
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
ctx.Data["PageIsLogin"] = false
ctx.Data["PageIsSignUp"] = false
ctx.Data["PageIsWechatQrLogin"] = false
}
// SignInWechatQr 渲染微信扫码登录页面 // SignInWechatQr 渲染微信扫码登录页面
func SignInWechatQr(ctx *context.Context) { func SignInWechatQr(ctx *context.Context) {
if CheckAutoLogin(ctx) { if CheckAutoLogin(ctx) {
@@ -177,14 +187,15 @@ func SignInWechatQr(ctx *context.Context) {
RedirectAfterLogin(ctx) RedirectAfterLogin(ctx)
return return
} }
SignInNavbarSetting(ctx)
ctx.Data["Title"] = ctx.Tr("sign_in") ctx.Data["Title"] = ctx.Tr("sign_in")
ctx.Data["SignInWechatQrLink"] = setting.AppSubURL + "/user/login/wechat/official-account" ctx.Data["SignInWechatQrLink"] = setting.AppSubURL + "/user/login/wechat/official-account"
ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLogin"] = true
ctx.Data["PageIsWechatQrLogin"] = true ctx.Data["PageIsWechatQrLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
_, err := GenerateWechatQr(ctx) wechatQrTicket, wechatQrCodeUrl, err := auth_service.GetWechatQRTicket(ctx)
if err != nil { if err != nil {
wechatQrFallbackLoginURL := "/user/login" wechatQrFallbackLoginURL := "/user/login"
redirectTo := ctx.FormString("redirect_to") redirectTo := ctx.FormString("redirect_to")
@@ -196,6 +207,10 @@ func SignInWechatQr(ctx *context.Context) {
return return
} }
ctx.Data["wechatQrTicket"] = wechatQrTicket
ctx.Data["wechatQrCodeUrl"] = wechatQrCodeUrl
ctx.Data["wechatQrExpireSeconds"] = setting.Wechat.OfficialAccount.TempQrExpireSeconds
ctx.HTML(http.StatusOK, tplSignInWechatQr) ctx.HTML(http.StatusOK, tplSignInWechatQr)
} }
@@ -212,6 +227,8 @@ func SignIn(ctx *context.Context) {
return return
} }
SignInNavbarSetting(ctx)
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
@@ -222,8 +239,6 @@ func SignIn(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login" ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLogin"] = true ctx.Data["PageIsLogin"] = true
ctx.Data["PageIsPasswordLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
context.SetCaptchaData(ctx) context.SetCaptchaData(ctx)
@@ -478,9 +493,10 @@ func SignOut(ctx *context.Context) {
// SignUp render the register page // SignUp render the register page
func SignUp(ctx *context.Context) { func SignUp(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_up")
ctx.Data["Title"] = ctx.Tr("sign_up")
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
SignInNavbarSetting(ctx)
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil { if err != nil {

View File

@@ -39,6 +39,8 @@ func SignInOpenID(ctx *context.Context) {
return return
} }
SignInNavbarSetting(ctx)
ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLoginOpenID"] = true ctx.Data["PageIsLoginOpenID"] = true
ctx.HTML(http.StatusOK, tplSignInOpenID) ctx.HTML(http.StatusOK, tplSignInOpenID)

View File

@@ -1,58 +0,0 @@
package auth
import (
"encoding/base64"
binaryUtils "encoding/binary"
"strings"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
"github.com/google/uuid"
)
// Define a Wechat Error type message
type WechatError struct {
message string
}
// Implement the Error() method for the `WechatError` type
func (e *WechatError) Error() string {
return e.message
}
/**
* 生成微信临时二维码
*
* @param ctx 页面会话上下文环境
* @return string 生成的微信二维码的 ticket
* @return error 如果生成二维码过程中出现错误,则返回相应的错误信息
*/
func GenerateWechatQr(ctx *context.Context) (wechatQrTicket string, errorGenerateQr error) {
if setting.Wechat.OfficialAccount.PowerWechat == nil {
log.Warn("PowerWechat工具类配置错误, 不会生成公众号带参数二维码")
return "", &WechatError{message: "ERROR: PowerWechat 配置错误 (PowerWechat app instance has not yet been initialized!)"}
}
// 生成随机 sceneStr 场景值
// sceneStr生成规则UUIDv4后边拼接 当前UnixNano时间戳转为byte数组后的Base64
// e.g, sceneStr == "1c78e8d914fb4307a3588ac0f6bc092a@yPXAm+ve5hc="
bytesArrayUnit64 := make([]byte, 8)
binaryUtils.LittleEndian.PutUint64(bytesArrayUnit64, uint64(time.Now().UnixNano()))
currentTimestampNanoBase64 := base64.StdEncoding.EncodeToString(bytesArrayUnit64)
sceneStr := strings.ReplaceAll(uuid.New().String(), "-", "") + "@" + currentTimestampNanoBase64
qrExpireSeconds := setting.Wechat.OfficialAccount.TempQrExpireSeconds
qrData, err := setting.Wechat.OfficialAccount.PowerWechat.QRCode.Temporary(ctx, sceneStr, qrExpireSeconds)
if err == nil {
wechatQrTicket = qrData.Ticket
ctx.Data["wechatQrTicket"] = wechatQrTicket
ctx.Data["wechatQrCodeUrl"] = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + wechatQrTicket
ctx.Data["wechatQrExpireSeconds"] = qrData.ExpireSeconds
} else {
log.Warn(" [!] 无法生成微信公众号带参数临时二维码: %s", err.Error())
}
return wechatQrTicket, err
}

View File

@@ -18,8 +18,8 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
web_auth_utils "code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/db" "code.gitea.io/gitea/services/auth/source/db"
"code.gitea.io/gitea/services/auth/source/smtp" "code.gitea.io/gitea/services/auth/source/smtp"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@@ -42,11 +42,15 @@ func Account(ctx *context.Context) {
loadAccountData(ctx) loadAccountData(ctx)
// 界面原型:更新微信,展示公众号带参数二维码 // 界面原型:更新微信,展示公众号带参数二维码
_, err := web_auth_utils.GenerateWechatQr(ctx) wechatQrTicket, wechatQrCodeUrl, err := auth_service.GetWechatQRTicket(ctx)
if err != nil { if err != nil {
log.Warn("微信创建二维码失败,跳过") log.Warn("微信创建二维码失败,跳过")
} }
ctx.Data["wechatQrTicket"] = wechatQrTicket
ctx.Data["wechatQrCodeUrl"] = wechatQrCodeUrl
ctx.Data["wechatQrExpireSeconds"] = setting.Wechat.OfficialAccount.TempQrExpireSeconds
ctx.HTML(http.StatusOK, tplSettingsAccount) ctx.HTML(http.StatusOK, tplSettingsAccount)
} }

200
services/auth/wechat_qr.go Normal file
View File

@@ -0,0 +1,200 @@
package auth
import (
"encoding/base64"
binaryUtils "encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/services/context"
wechat_official_account_user_model "code.gitea.io/gitea/models/wechat"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
wechat_official_account_cache_service "code.gitea.io/gitea/services/wechat/cache"
wechat_official_account_vo "code.gitea.io/gitea/services/wechat/vo"
"github.com/google/uuid"
)
// Define a Wechat Error type message
type WechatError struct {
message string
}
// Implement the Error() method for the `WechatError` type
func (e *WechatError) Error() string {
return e.message
}
type ResponseData struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Ticket string `json:"ticket"`
ExpireSeconds int `json:"expire_seconds"`
URL string `json:"url"`
QRImageURL string `json:"qr_image_url"`
} `json:"data"`
}
/**
* GetWechatQRTicket 生成微信官方账户临时二维码
*
* @return string 生成的微信二维码的 ticket
* @return string 生成的微信二维码图片URL
* @return error 如果生成二维码过程中出现错误,则返回相应的错误信息
*/
func GetWechatQRTicket(ctx *context.Context) (wechatQrTicket string, QRImageURL string, errorGenerateQr error) {
// 生成随机 sceneStr 场景值
// sceneStr生成规则UUIDv4后边拼接 当前UnixNano时间戳转为byte数组后的Base64
// e.g, sceneStr == "1c78e8d914fb4307a3588ac0f6bc092a@yPXAm+ve5hc="
bytesArrayUnit64 := make([]byte, 8)
binaryUtils.LittleEndian.PutUint64(bytesArrayUnit64, uint64(time.Now().UnixNano()))
currentTimestampNanoBase64 := base64.StdEncoding.EncodeToString(bytesArrayUnit64)
sceneStr := strings.ReplaceAll(uuid.New().String(), "-", "") + "@" + currentTimestampNanoBase64
qrExpireSeconds := setting.Wechat.OfficialAccount.TempQrExpireSeconds
// 构建请求的 URL
url := fmt.Sprintf("https://%s/api/wechat/official-account/login/qr/generate?qrExpireSeconds=%d&sceneStr=%s", setting.Wechat.OfficialAccount.DefaultDomainName, qrExpireSeconds, sceneStr)
// 发送 GET 请求
resp, err := http.Get(url)
if err != nil {
return "", "", fmt.Errorf("failed to send GET request: %v", err)
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", fmt.Errorf("failed to read response body: %v", err)
}
// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return "", "", fmt.Errorf("received non-200 response status: %s", resp.Status)
}
// 解析 JSON 响应
var data ResponseData
if err := json.Unmarshal(bodyBytes, &data); err != nil {
return "", "", fmt.Errorf("failed to unmarshal JSON response: %v", err)
}
quit := make(chan bool) // 用来通知goroutine停止
// 启动一个新的goroutine来进行轮询
go func() {
// 创建一个超时定时器通道
timeout := time.After(time.Duration(qrExpireSeconds) * time.Second)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
checkWechatQrTicketStatus(ctx, wechatQrTicket, quit)
case <-quit:
log.Info("Stopping polling...")
return
case <-timeout:
log.Info("Polling timed out after", qrExpireSeconds, "seconds.")
quit <- true // 发送停止信号给轮询goroutine
}
}
}()
return data.Data.Ticket, data.Data.QRImageURL, nil
}
// Response represents the top-level JSON structure
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data Data `json:"data"`
}
// Data represents the "data" object in the JSON
type Data struct {
IsScanned bool `json:"is_scanned"`
SceneStr string `json:"scene_str"`
OpenID string `json:"openid"`
IsBinded bool `json:"is_binded"`
}
// 假设这是用于检查二维码状态的函数
func checkWechatQrTicketStatus(ctx *context.Context, qrTicket string, quit chan bool) {
url := fmt.Sprintf("https://%s/api/wechat/official-account/login/qr/check-status?ticket=%s&_=%d",
setting.Wechat.OfficialAccount.DefaultDomainName, qrTicket, time.Now().UnixMilli())
resp, err := http.Get(url)
if err != nil {
log.Error("There was a problem with the fetch operation:", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Error("Network response was not ok")
return
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
log.Error("Error reading response body:", err)
return
}
// 解析 JSON 响应
var data Response
if err := json.Unmarshal(bodyBytes, &data); err != nil {
log.Error("failed to unmarshal JSON response: %v", err)
return
}
if data.Code == 0 && data.Data.IsScanned {
log.Info("Caching WeChat QR Scanned Info: %s\n", bodyBytes)
// {"code":0,"msg":"操作成功","data":{
// "is_scanned":true,
// "scene_str":"2d521f80047c42aba27ee9beade35985@p2Z6hfheDxg=",
// "openid":"oQowJ6cD9WSuoxYaCc7mryfn-lVo",
// "is_binded":true}}
// 准备扫码状态VO对象
qrStatusVO := wechat_official_account_vo.GetWechatOfficialAccountTempQRStatusVO{
SceneStr: data.Data.SceneStr,
IsScanned: data.Data.IsScanned,
OpenId: data.Data.OpenID,
}
// 从微信服务器消息推送中解析扫码人的 OpenID
_, err := wechat_official_account_user_model.QueryUserByOpenid(ctx, qrStatusVO.OpenId)
if err != nil {
// 未找到 OpenID 对应的 DevStar 用户信息,提示前端导向注册页
qrStatusVO.IsBinded = false
qrStatusVOString, err := qrStatusVO.Marshal2JSONString()
if err == nil {
// 将扫码人的微信公众号 OpenID 标记为等待注册等待时间用户注册完成默认24小时
// key: qrScanResponseDigest.Ticket
// value: JSON 字符串
// TTL: setting.Wechat.OfficialAccount.TempQrExpireSeconds
wechat_official_account_cache_service.SetWechatOfficialAccountQrTicketWithTTL(
qrTicket,
qrStatusVOString,
setting.Wechat.OfficialAccount.RegisterationExpireSeconds)
}
quit <- true
return
}
qrStatusVO.IsBinded = true
qrVOJsonString, err := qrStatusVO.Marshal2JSONString()
if err == nil {
wechat_official_account_cache_service.SetWechatOfficialAccountQrTicketWithTTL(
qrTicket,
qrVOJsonString,
setting.Wechat.OfficialAccount.TempQrExpireSeconds)
}
quit <- true
return
}
}

View File

@@ -158,6 +158,7 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
ctx.Data["EnableWechatQrLogin"] = setting.Wechat.OfficialAccount.Enabled
ctx.Data["Link"] = ctx.Link ctx.Data["Link"] = ctx.Link
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules // PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules

View File

@@ -52,6 +52,7 @@ type InstallForm struct {
EnableFederatedAvatar bool EnableFederatedAvatar bool
EnableOpenIDSignIn bool EnableOpenIDSignIn bool
EnableOpenIDSignUp bool EnableOpenIDSignUp bool
EnableWechatQRSignIn bool
DisableRegistration bool DisableRegistration bool
AllowOnlyExternalRegistration bool AllowOnlyExternalRegistration bool
EnableCaptcha bool EnableCaptcha bool

View File

@@ -362,7 +362,7 @@ func GenerateGitContentFromGitURL(ctx context.Context, gitURL, commitID string,
log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err) log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
return fmt.Errorf("git remote add: %w", err) return fmt.Errorf("git remote add: %w", err)
} }
commitMessage := "Initial commit from " + gitURL + "(" + sha1 + ")" commitMessage := "Initial commit from " + gitURL + " ( " + sha1 + " ) "
initRepoCommit(ctx, tmpDir, repo, repo.Owner, repo.DefaultBranch, commitMessage) initRepoCommit(ctx, tmpDir, repo, repo.Owner, repo.DefaultBranch, commitMessage)
if err := repo_module.UpdateRepoSize(ctx, repo); err != nil { if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {

View File

@@ -177,7 +177,7 @@
{{svg "octicon-person"}} {{ctx.Locale.Tr "register"}} {{svg "octicon-person"}} {{ctx.Locale.Tr "register"}}
</a> </a>
{{end}} {{end}}
<a class="item{{if .PageIsSignIn}} active{{end}}" rel="nofollow" href="{{AppSubUrl}}/user/login/wechat/official-account{{if not .PageIsSignIn}}?redirect_to={{.CurrentURL}}{{end}}"> <a class="item{{if .PageIsSignIn}} active{{end}}" rel="nofollow" href="{{AppSubUrl}}/user/login{{if .EnableWechatQrLogin}}/wechat/official-account{{end}}{{if not .PageIsSignIn}}?redirect_to={{.CurrentURL}}{{end}}">
{{svg "octicon-sign-in"}} {{ctx.Locale.Tr "sign_in"}} {{svg "octicon-sign-in"}} {{ctx.Locale.Tr "sign_in"}}
</a> </a>
{{end}} {{end}}

View File

@@ -221,12 +221,6 @@
<input name="enable_federated_avatar" type="checkbox" {{if .enable_federated_avatar}}checked{{end}}> <input name="enable_federated_avatar" type="checkbox" {{if .enable_federated_avatar}}checked{{end}}>
</div> </div>
</div> </div>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signin">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label>
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}>
</div>
</div>
<div class="inline field"> <div class="inline field">
<div class="ui checkbox" id="disable-registration"> <div class="ui checkbox" id="disable-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label> <label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label>
@@ -245,6 +239,18 @@
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}> <input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}>
</div> </div>
</div> </div>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signin">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label>
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-wechat-qr-signin">
<label data-tooltip-content="{{ctx.Locale.Tr "install.wechat_qr_signin_popup"}}">{{ctx.Locale.Tr "install.wechat_qr_signin"}}</label>
<input name="enable_wechat_qr_sign_in" type="checkbox" {{if .enable_wechat_qr_sign_in}}checked{{end}}>
</div>
</div>
<div class="inline field"> <div class="inline field">
<div class="ui checkbox" id="enable-captcha"> <div class="ui checkbox" id="enable-captcha">
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label> <label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label>

View File

@@ -1,44 +1,29 @@
{{if or .EnableOpenIDSignIn .EnableSSPI .EnableWechatQrLogin}}
<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar secondary-nav"> <overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar secondary-nav">
<div class="overflow-menu-items tw-justify-center"> <div class="overflow-menu-items tw-justify-center">
<a class="{{if .PageIsLogin}}active {{end}}item" rel="nofollow" href="{{AppSubUrl}}/user/login">
{{/* */}} {{ctx.Locale.Tr "auth.login_userpass"}}
{{if .PageIsLogin}} </a>
<a class="{{if .PageIsWechatQrLogin}} active {{end}} item" rel="nofollow" href="{{AppSubUrl}}/user/login/wechat/official-account"> <a class="{{if .PageIsSignUp}}active{{end}} item" rel="nofollow" href="{{AppSubUrl}}/user/sign_up">
{{ctx.Locale.Tr "auth.create_new_account"}}
</a>
{{if .EnableWechatQrLogin}}
<a class="{{if .PageIsWechatQrLogin}}active {{end}} item" rel="nofollow" href="{{AppSubUrl}}/user/login/wechat/official-account">
{{ctx.Locale.Tr "settings.wechat_qr_login"}} {{ctx.Locale.Tr "settings.wechat_qr_login"}}
</a>
{{end}}
{{if .EnableOpenIDSignIn}}
<a class="{{if .PageIsLoginOpenID}}active {{end}}item" rel="nofollow" href="{{AppSubUrl}}/user/login/openid">
{{svg "fontawesome-openid"}}
&nbsp;OpenID
</a> </a>
{{end}} {{end}}
{{if .EnableSSPI}}
{{/* 2. */}} <a class="item" rel="nofollow" href="{{AppSubUrl}}/user/login?auth_with_sspi=1">
{{if .PageIsLogin}} {{svg "fontawesome-windows"}}
<a class="{{if .PageIsPasswordLogin}} active {{end}} item" rel="nofollow" href="{{AppSubUrl}}/user/login"> &nbsp;SSPI
{{ctx.Locale.Tr "password"}}
</a> </a>
{{end}}
{{/* 3. */}}
{{/* <a class="{{if .PageIsSmsLogin}} active {{end}} item" rel="nofollow" href="{{AppSubUrl}}/user/login/sms"> */}}
{{/* {{ctx.Locale.Tr "settings.phone_sms_code"}} */}}
{{/* </a> */}}
{{if or .EnableOpenIDSignIn .EnableSSPI}}
<a class="{{if .PageIsLogin}}active {{end}}item" rel="nofollow" href="{{AppSubUrl}}/user/login">
{{ctx.Locale.Tr "auth.login_userpass"}}
</a>
<a class="{{if .PageIsSignUp}}active{{end}} item" rel="nofollow" href="{{AppSubUrl}}/user/sign_up">
{{ctx.Locale.Tr "auth.create_new_account"}}
</a>
{{if .EnableOpenIDSignIn}}
<a class="{{if .PageIsLoginOpenID}}active {{end}}item" rel="nofollow" href="{{AppSubUrl}}/user/login/openid">
{{svg "fontawesome-openid"}}
&nbsp;OpenID
</a>
{{end}}
{{if .EnableSSPI}}
<a class="item" rel="nofollow" href="{{AppSubUrl}}/user/login?auth_with_sspi=1">
{{svg "fontawesome-windows"}}
&nbsp;SSPI
</a>
{{end}}
{{end}} {{end}}
</div> </div>
</overflow-menu> </overflow-menu>
{{end}}