143 lines
4.5 KiB
Go
143 lines
4.5 KiB
Go
package wechatmp
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"server/models"
|
|
|
|
beego "github.com/beego/beego/v2/server/web"
|
|
)
|
|
|
|
// =============================================================
|
|
// 公众号微信登录(免认证方案):
|
|
// 用户在公众号内发送「登录」→ 后端校验绑定关系并生成一次性登录令牌 →
|
|
// 被动回复登录链接 → 用户在微信内打开链接 → 前端 /mp-login 页面
|
|
// 用令牌调用 /api/wechat/mp/login 换取正式登录态。
|
|
// 未认证公众号无法使用网页授权(snsapi_*),一次性链接是替代方案。
|
|
// =============================================================
|
|
|
|
// DefaultLoginTokenTTL 登录令牌默认有效期
|
|
const DefaultLoginTokenTTL = 5 * time.Minute
|
|
|
|
var (
|
|
ErrLoginTokenInvalid = errors.New("登录链接无效或已过期,请在公众号重新发送「登录」")
|
|
ErrLoginTokenUsed = errors.New("登录链接已被使用,请在公众号重新发送「登录」")
|
|
ErrLoginTokenExpired = errors.New("登录链接已过期,请在公众号重新发送「登录」")
|
|
)
|
|
|
|
func randomTokenHex(nBytes int) string {
|
|
b := make([]byte, nBytes)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return ""
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
// LoginLinkURL 由一次性令牌拼出前端登录页链接。
|
|
// 页面基址读取 app.conf 的 wechat_mp_login_base(前端应用对外可访问地址,
|
|
// 如 https://back.yunzer.cn),缺省回落 wechat_mp_callback_base / payment_callback_base。
|
|
// 未配置时返回空串,由调用方给出提示。
|
|
func LoginLinkURL(token string) string {
|
|
base, _ := beego.AppConfig.String("wechat_mp_login_base")
|
|
if strings.TrimSpace(base) == "" {
|
|
base, _ = beego.AppConfig.String("wechat_mp_callback_base")
|
|
}
|
|
if strings.TrimSpace(base) == "" {
|
|
base, _ = beego.AppConfig.String("payment_callback_base")
|
|
}
|
|
base = strings.TrimRight(strings.TrimSpace(base), "/")
|
|
if base == "" {
|
|
return ""
|
|
}
|
|
return base + "/#/mp-login?token=" + url.QueryEscape(token)
|
|
}
|
|
|
|
// CreateLoginToken 为已绑定的粉丝生成一次性登录令牌。
|
|
func CreateLoginToken(f *models.WechatMpFollower, ttl time.Duration) (token string, expireAt time.Time, err error) {
|
|
if f == nil || strings.TrimSpace(f.BindType) == "" || f.BindID == 0 {
|
|
return "", time.Time{}, errors.New("该微信尚未绑定账号")
|
|
}
|
|
if ttl <= 0 {
|
|
ttl = DefaultLoginTokenTTL
|
|
}
|
|
token = randomTokenHex(32)
|
|
if token == "" {
|
|
return "", time.Time{}, errors.New("生成登录令牌失败")
|
|
}
|
|
now := time.Now()
|
|
expireAt = now.Add(ttl)
|
|
row := &models.WechatMpLoginToken{
|
|
Token: token,
|
|
OpenID: f.OpenID,
|
|
BindType: f.BindType,
|
|
BindID: f.BindID,
|
|
BindTid: f.BindTid,
|
|
ExpireAt: expireAt,
|
|
CreateTime: now,
|
|
}
|
|
if _, err := models.Orm.Insert(row); err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
return token, expireAt, nil
|
|
}
|
|
|
|
// ConsumeLoginToken 核销一次性登录令牌(CAS 防重放),返回令牌携带的绑定信息。
|
|
func ConsumeLoginToken(token string) (*models.WechatMpLoginToken, error) {
|
|
token = strings.TrimSpace(token)
|
|
if token == "" {
|
|
return nil, ErrLoginTokenInvalid
|
|
}
|
|
var row models.WechatMpLoginToken
|
|
if err := models.Orm.QueryTable(new(models.WechatMpLoginToken)).
|
|
Filter("token", token).One(&row); err != nil {
|
|
return nil, ErrLoginTokenInvalid
|
|
}
|
|
if row.UsedAt != nil {
|
|
return nil, ErrLoginTokenUsed
|
|
}
|
|
if row.ExpireAt.Before(time.Now()) {
|
|
return nil, ErrLoginTokenExpired
|
|
}
|
|
now := time.Now()
|
|
affected, err := models.Orm.QueryTable(new(models.WechatMpLoginToken)).
|
|
Filter("id", row.ID).
|
|
Filter("used_at__isnull", true).
|
|
Update(map[string]interface{}{"used_at": now})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if affected == 0 {
|
|
return nil, ErrLoginTokenUsed
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
// isLoginKeyword 判断文本是否为「登录」意图
|
|
func isLoginKeyword(text string) bool {
|
|
switch strings.TrimSpace(text) {
|
|
case "登录", "登陸", "login", "Login", "微信登录", "微信登陆":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isLoginMenuKey 判断自定义菜单点击(CLICK 事件)的 EventKey 是否为「登录」入口。
|
|
// 菜单在公众号后台手动配置即可(未认证公众号同样支持),
|
|
// 用户点菜单 → CLICK 事件 → 自动回复确认登录链接,免输入。
|
|
func isLoginMenuKey(key string) bool {
|
|
k := strings.TrimSpace(key)
|
|
if k == "" {
|
|
return false
|
|
}
|
|
switch strings.ToLower(k) {
|
|
case "login", "登录", "扫码登录", "微信登录":
|
|
return true
|
|
}
|
|
return false
|
|
}
|