Files
yunzerwebsiteallinone/go/controllers/auth/wechat_scan.go
T
2026-09-30 00:57:36 +08:00

144 lines
4.9 KiB
Go

package auth
import (
"net/url"
"strings"
"time"
"server/models"
authsvc "server/services/auth"
"server/services/wechatmp"
)
// =============================================================
// 统一认证中心「微信扫码登录」(未认证公众号方案):
// 登录页展示「服务号关注二维码 + 6 位登录码」→ 用户微信扫码进入公众号会话
// → 发送登录码 → 服务号回复「确认登录」链接 → 用户点击(/mp-login 兑换时
// 携带 scene 回写确认)→ 本组接口轮询确认后建立 UAC 会话 Cookie,
// 前端跳回 authorize 完成 OAuth 发放。全程无极验验证码。
// =============================================================
// WechatScanStart POST /auth/wechat/scan/start
// 发起扫码登录会话并生成带参二维码(认证公众号);未认证回退普通关注二维码。
// 用户扫码后公众号自动回复「确认登录」链接,无需输入任何登录码。
func (c *AuthLoginController) WechatScanStart() {
scene, qrURL, notice, expireAt, err := wechatmp.StartScanLogin()
if err != nil {
c.serveJSON(map[string]interface{}{"code": 500, "msg": "发起扫码登录失败:" + err.Error()})
return
}
c.serveJSON(map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"scene": scene,
"qrcode_url": qrURL, // 带参二维码(认证公众号);空 = 已回退
"follow_qrcode": wechatmp.FollowQrcodeURL(),
"notice": notice, // 回退原因(非空时前端醒目提示)
"expire_in": int(time.Until(expireAt).Seconds()),
},
})
}
// WechatScanStatus GET /auth/wechat/scan/status?scene=xxx&redirect=yyy
// 登录页轮询:待确认 status=0;已确认 → 建立 UAC 会话(写 Cookie)并返回回跳地址
// (多企业时回跳企业选择步骤);过期 status=2。
func (c *AuthLoginController) WechatScanStatus() {
redirectParam := c.GetString("redirect")
clientID := strings.TrimSpace(c.GetString("client_id"))
row, err := wechatmp.PickScanLogin(c.GetString("scene"))
if err != nil {
if err == wechatmp.ErrScanLoginInvalid {
c.serveJSON(map[string]interface{}{"code": 200, "data": map[string]interface{}{"status": 2}})
return
}
// 待确认等业务提示:status=0 继续轮询
c.serveJSON(map[string]interface{}{"code": 200, "data": map[string]interface{}{"status": 0, "msg": err.Error()}})
return
}
// 身份状态校验(禁用/锁定则拒绝)
var identity models.AuthIdentity
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
Filter("id", row.BindID).One(&identity); err != nil || identity.Status != models.AuthIdentityStatusEnabled {
c.serveJSON(map[string]interface{}{"code": 400, "msg": "账号已被禁用或锁定,无法登录"})
return
}
// 企业选择:单企业直接进入;多企业进入登录页企业选择步骤
tenants, lerr := authsvc.ListTenantOptions(row.BindID)
if lerr != nil || len(tenants) == 0 {
c.serveJSON(map[string]interface{}{"code": 400, "msg": "该账号未加入任何企业,无法登录"})
return
}
tid := uint64(authsvc.PendingTenantID)
if len(tenants) == 1 {
tid = tenants[0].Tid
}
username := "微信用户"
if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" {
username = *identity.Nickname
}
ip := c.Ctx.Input.IP()
userAgent := c.Ctx.Request.UserAgent()
// 建立 UAC 会话并写 Cookie(与账号密码登录同一套会话体系,authorize 据此发码)
sess, serr := authsvc.CreateSession(authsvc.SessionInfo{
IdentityID: row.BindID,
Tid: tid,
ClientID: clientID,
IP: ip,
UserAgent: userAgent,
LoginType: "wechat_mp",
Amr: "wechat_mp",
})
if serr != nil {
c.serveJSON(map[string]interface{}{"code": 400, "msg": "登录失败:" + serr.Error()})
return
}
if _, terr := authsvc.IssueTokens(authsvc.TokenIssue{
IdentityID: row.BindID,
Tid: tid,
ClientID: clientID,
Sid: sess.Sid,
Username: username,
Amr: "wechat_mp",
}); terr != nil {
c.serveJSON(map[string]interface{}{"code": 500, "msg": "签发令牌失败:" + terr.Error()})
return
}
setSessionCookieForCtx(c.Ctx, sess.Sid)
// 登录日志(失败不影响主流程)
identityID := row.BindID
amr := "wechat_mp"
_, _ = models.Orm.Insert(&models.AuthLoginLog{
Tid: &tid,
IdentityID: &identityID,
UserName: username,
ClientID: clientID,
LoginType: "wechat_mp",
Amr: &amr,
Status: 1,
Message: "公众号扫码登录成功",
IP: ip,
UserAgent: userAgent,
})
// 默认回跳到原 authorize;多企业时回跳登录页企业选择步骤
back := decodeRedirect(redirectParam)
if len(tenants) > 1 {
back = "/auth/login?step=tenant&client_id=" + url.QueryEscape(clientID) +
"&redirect=" + url.QueryEscape(redirectParam)
}
c.serveJSON(map[string]interface{}{
"code": 200,
"data": map[string]interface{}{
"status": 1,
"redirect": back,
},
})
}