244 lines
6.6 KiB
Go
244 lines
6.6 KiB
Go
package auth
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"strings"
|
||
"time"
|
||
|
||
"server/models"
|
||
"server/pkg/jwtutil"
|
||
authsvc "server/services/auth"
|
||
|
||
beego "github.com/beego/beego/v2/server/web"
|
||
)
|
||
|
||
// =============================================================
|
||
// 移动端免登录票据(把移动端登录态桥接到 Web 端)
|
||
//
|
||
// POST /auth/app-ticket 移动端带 Bearer 令牌 → 签发一次性票据
|
||
// POST /auth/app-ticket/exchange Web 端带票据 → 核销并换发 Web 端令牌
|
||
//
|
||
// 仅用于「移动端已登录 → 打开 Web 端功能页」这一条链路,
|
||
// 票据一次性 + 60 秒有效,泄露也无法二次使用。
|
||
// =============================================================
|
||
|
||
// AuthAppTicketController 移动端免登录票据
|
||
type AuthAppTicketController struct {
|
||
beego.Controller
|
||
}
|
||
|
||
func (c *AuthAppTicketController) serveJSON(data map[string]interface{}) {
|
||
c.Data["json"] = data
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
func (c *AuthAppTicketController) fail(msg string) {
|
||
c.Ctx.Output.SetStatus(400)
|
||
c.serveJSON(map[string]interface{}{"code": 400, "msg": msg})
|
||
}
|
||
|
||
// ticketClaims 解析 Bearer 令牌;移动端是 user_type=app,Web 端是 backend
|
||
func (c *AuthAppTicketController) ticketClaims() (*jwtutil.Claims, error) {
|
||
header := c.Ctx.Request.Header.Get("Authorization")
|
||
if header == "" {
|
||
return nil, fmt.Errorf("未登录")
|
||
}
|
||
parts := strings.SplitN(header, " ", 2)
|
||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||
return nil, fmt.Errorf("认证信息格式错误")
|
||
}
|
||
claims, err := jwtutil.ParseToken(parts[1])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("无效的token")
|
||
}
|
||
if claims.UserType != "app" && claims.UserType != "backend" {
|
||
return nil, fmt.Errorf("无权访问")
|
||
}
|
||
return claims, nil
|
||
}
|
||
|
||
// Create POST /auth/app-ticket
|
||
// 移动端用当前登录态换取一次性票据,Web 端拿票据换自己的令牌。
|
||
func (c *AuthAppTicketController) Create() {
|
||
claims, err := c.ticketClaims()
|
||
if err != nil {
|
||
c.Ctx.Output.SetStatus(401)
|
||
c.serveJSON(map[string]interface{}{"code": 401, "msg": err.Error()})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
ClientID string `json:"client_id"`
|
||
}
|
||
if body := c.Ctx.Input.RequestBody; len(body) > 0 {
|
||
_ = json.Unmarshal(body, &req)
|
||
}
|
||
// 票据是给 Web 端兑换用的,默认按 Web 端应用签发
|
||
clientID := strings.TrimSpace(req.ClientID)
|
||
if clientID == "" {
|
||
clientID = "yz-backend"
|
||
}
|
||
|
||
tid := uint64(0)
|
||
if claims.TenantId > 0 {
|
||
tid = uint64(claims.TenantId)
|
||
}
|
||
|
||
ticket, err := authsvc.CreateAppTicket(uint64(claims.UserID), tid, clientID)
|
||
if err != nil {
|
||
c.fail("签发票据失败:" + err.Error())
|
||
return
|
||
}
|
||
|
||
c.serveJSON(map[string]interface{}{
|
||
"code": 200,
|
||
"msg": "success",
|
||
"data": map[string]interface{}{
|
||
"ticket": ticket,
|
||
"expire_in": int(authsvc.AppTicketTTL / time.Second),
|
||
},
|
||
})
|
||
}
|
||
|
||
// Exchange POST /auth/app-ticket/exchange
|
||
// Web 端核销票据并换发本端令牌(无鉴权:票据本身即凭证,一次性 + 短时效)。
|
||
func (c *AuthAppTicketController) Exchange() {
|
||
body := c.Ctx.Input.RequestBody
|
||
if len(body) == 0 {
|
||
body, _ = io.ReadAll(c.Ctx.Request.Body)
|
||
}
|
||
var req struct {
|
||
Ticket string `json:"ticket"`
|
||
Token string `json:"token"` // 兼容 mp-login 的命名
|
||
ClientID string `json:"client_id"`
|
||
}
|
||
if err := json.Unmarshal(body, &req); err != nil {
|
||
c.fail("参数错误")
|
||
return
|
||
}
|
||
ticket := strings.TrimSpace(req.Ticket)
|
||
if ticket == "" {
|
||
ticket = strings.TrimSpace(req.Token)
|
||
}
|
||
|
||
row, err := authsvc.ConsumeAppTicket(ticket)
|
||
if err != nil {
|
||
c.fail(err.Error())
|
||
return
|
||
}
|
||
|
||
// 身份校验:禁用/锁定的账号不签发
|
||
var identity models.AuthIdentity
|
||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||
Filter("id", row.IdentityID).
|
||
Filter("delete_time__isnull", true).
|
||
One(&identity); err != nil {
|
||
c.fail("账号不存在或已注销")
|
||
return
|
||
}
|
||
if identity.Status != models.AuthIdentityStatusEnabled {
|
||
c.fail("账号已被禁用或锁定,无法登录")
|
||
return
|
||
}
|
||
|
||
username := "云泽用户"
|
||
if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" {
|
||
username = strings.TrimSpace(*identity.Nickname)
|
||
}
|
||
|
||
clientID := strings.TrimSpace(req.ClientID)
|
||
if clientID == "" {
|
||
clientID = row.ClientID
|
||
}
|
||
if clientID == "" {
|
||
clientID = "yz-backend"
|
||
}
|
||
|
||
// 未确定企业(多企业)时签发待选令牌,由 Web 端展示企业选择
|
||
tid := row.Tid
|
||
needChoose := false
|
||
if tid == 0 {
|
||
tenants, lerr := authsvc.ListTenantOptions(row.IdentityID)
|
||
if lerr != nil || len(tenants) == 0 {
|
||
c.fail("该账号未加入任何企业,无法登录")
|
||
return
|
||
}
|
||
if len(tenants) == 1 {
|
||
tid = tenants[0].Tid
|
||
} else {
|
||
tid = authsvc.PendingTenantID
|
||
needChoose = true
|
||
}
|
||
}
|
||
|
||
tokens, terr := c.issueTicketTokens(row.IdentityID, tid, clientID, username, needChoose)
|
||
if terr != nil {
|
||
c.fail(terr.Error())
|
||
return
|
||
}
|
||
|
||
data := map[string]interface{}{
|
||
"type": "backend",
|
||
"tokens": tokens,
|
||
"tid": tid,
|
||
}
|
||
if needChoose {
|
||
tenants, _ := authsvc.ListTenantOptions(row.IdentityID)
|
||
data["need_choose_tenant"] = true
|
||
data["tenants"] = tenants
|
||
}
|
||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "登录成功", "data": data})
|
||
}
|
||
|
||
// issueTicketTokens 建会话 + 签发令牌 + 登录日志(失败不影响主流程)
|
||
func (c *AuthAppTicketController) issueTicketTokens(identityID, tid uint64, clientID, username string, needChoose bool) (*authsvc.TokenPair, error) {
|
||
accessTTL := 0
|
||
if needChoose {
|
||
accessTTL = 600 // 待选状态令牌只够用户完成企业选择
|
||
}
|
||
sess, serr := authsvc.CreateSession(authsvc.SessionInfo{
|
||
IdentityID: identityID,
|
||
Tid: tid,
|
||
ClientID: clientID,
|
||
IP: c.Ctx.Input.IP(),
|
||
UserAgent: c.Ctx.Request.UserAgent(),
|
||
LoginType: "app_ticket",
|
||
Amr: "app_ticket",
|
||
})
|
||
if serr != nil {
|
||
return nil, fmt.Errorf("登录失败:%s", serr.Error())
|
||
}
|
||
tokens, terr := authsvc.IssueTokens(authsvc.TokenIssue{
|
||
IdentityID: identityID,
|
||
Tid: tid,
|
||
ClientID: clientID,
|
||
Sid: sess.Sid,
|
||
Username: username,
|
||
UserType: "backend", // Web 端业务接口普遍要求 backend
|
||
Amr: "app_ticket",
|
||
AccessTTL: accessTTL,
|
||
})
|
||
if terr != nil {
|
||
return nil, fmt.Errorf("签发令牌失败:%s", terr.Error())
|
||
}
|
||
|
||
identityIDCopy := identityID
|
||
tidCopy := tid
|
||
amr := "app_ticket"
|
||
_, _ = models.Orm.Insert(&models.AuthLoginLog{
|
||
Tid: &tidCopy,
|
||
IdentityID: &identityIDCopy,
|
||
UserName: username,
|
||
ClientID: clientID,
|
||
LoginType: "app_ticket",
|
||
Amr: &amr,
|
||
Status: 1,
|
||
Message: "移动端票据免登录",
|
||
IP: c.Ctx.Input.IP(),
|
||
UserAgent: c.Ctx.Request.UserAgent(),
|
||
})
|
||
return tokens, nil
|
||
}
|