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, }, }) }