package service import ( "context" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" "golang.org/x/crypto/bcrypt" "service.xpcool.com/internal/consts" "service.xpcool.com/internal/dao" "service.xpcool.com/internal/library/jwt" "service.xpcool.com/internal/library/response" "service.xpcool.com/internal/model/do" "service.xpcool.com/internal/model/dto" "service.xpcool.com/internal/model/entity" ) // WechatResolver 抽象微信 code 换 openid,避免业务层依赖具体 HTTP 实现。 type WechatResolver interface { OpenID(context.Context, string) (string, error) } type SMSVerifier interface { Verify(context.Context, string, string) error } // userAuth 负责用户三种登录方式及令牌签发,不承担第三方平台通信细节。 type userAuth struct { tokens *jwt.Service wechat WechatResolver sms SMSVerifier } func NewUserAuth(tokens *jwt.Service, wechat WechatResolver, sms SMSVerifier) IUserAuth { return &userAuth{tokens, wechat, sms} } func (s *userAuth) Login(ctx context.Context, in dto.UserLoginInput) (*dto.TokenPair, uint64, error) { var u entity.User var err error switch in.LoginType { case "wechat": // 微信首次授权成功后按 openid 自动创建用户。 if s.wechat == nil { return nil, 0, response.Error(consts.CodeInternal, "wechat provider not configured") } openID, e := s.wechat.OpenID(ctx, in.Code) if e != nil { return nil, 0, gerror.Wrap(e, "wechat login") } err = dao.User.Ctx(ctx).Where(do.User{OpenId: openID}).Scan(&u) if err != nil { return nil, 0, gerror.Wrap(err, "query user") } if u.Id == 0 { id, e := dao.User.Ctx(ctx).Data(do.User{OpenId: openID, Nickname: "微信用户", Status: 1}).InsertAndGetId() if e != nil { return nil, 0, gerror.Wrap(e, "create user") } u.Id = uint64(id) } case "mobile": // 验证码由注入的服务校验,校验成功后按手机号自动注册。 if s.sms == nil { return nil, 0, response.Error(consts.CodeInternal, "sms provider not configured") } if err = s.sms.Verify(ctx, in.Mobile, in.VerifyCode); err != nil { return nil, 0, response.Error(consts.CodeInvalidParam, "invalid verification code") } err = dao.User.Ctx(ctx).Where(do.User{Mobile: in.Mobile}).Scan(&u) if err != nil { return nil, 0, gerror.Wrap(err, "query user") } if u.Id == 0 { id, e := dao.User.Ctx(ctx).Data(do.User{Mobile: in.Mobile, Nickname: "用户" + in.Mobile[7:], Status: 1}).InsertAndGetId() if e != nil { return nil, 0, gerror.Wrap(e, "create user") } u.Id = uint64(id) } case "password": // 密码只使用 bcrypt 比对哈希值,任何场景都不回传或记录明文。 err = dao.User.Ctx(ctx).Where(do.User{Account: in.Account}).Scan(&u) if err != nil { return nil, 0, gerror.Wrap(err, "query user") } if u.Id == 0 || bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(in.Password)) != nil { return nil, 0, response.Error(consts.CodeUserPasswordWrong, "account or password incorrect") } default: return nil, 0, response.Error(consts.CodeUserLoginType, "unsupported login type") } if u.Status != 1 { return nil, 0, response.Error(consts.CodeForbidden, "user disabled") } _, err = dao.User.Ctx(ctx).Where(do.User{Id: u.Id}).Data(do.User{LastLoginAt: gtime.Now()}).Update() if err != nil { return nil, 0, gerror.Wrap(err, "update login time") } return s.issue(ctx, u.Id, in.Terminal) } func (s *userAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair, uint64, error) { // 刷新时先校验 JWT,再以 JTI 原子撤销旧会话,实现单次使用的令牌轮换。 c, err := s.tokens.Parse(refresh, "refresh", "user") if err != nil { return nil, 0, response.Error(consts.CodeUnauthorized, "invalid refresh token") } result, err := dao.AuthRefreshSession.Ctx(ctx).Where(do.AuthRefreshSession{Jti: c.JTI}).WhereNull("revoked_at").Data(do.AuthRefreshSession{RevokedAt: gtime.Now()}).Update() if err != nil { return nil, 0, gerror.Wrap(err, "撤销旧刷新令牌失败") } affected, err := result.RowsAffected() if err != nil || affected != 1 { return nil, 0, response.Error(consts.CodeUnauthorized, "刷新令牌已失效") } return s.issue(ctx, c.Subject, c.Terminal) } // issue 签发 JWT 后把 refresh token 的 JTI 落库,确保可以撤销和追踪设备会话。 func (s *userAuth) issue(ctx context.Context, id uint64, terminal string) (*dto.TokenPair, uint64, error) { a, r, exp, err := s.tokens.Issue(id, "user", terminal) if err != nil { return nil, 0, gerror.Wrap(err, "issue token") } claims, err := s.tokens.Parse(r, "refresh", "user") if err != nil { return nil, 0, gerror.Wrap(err, "解析新刷新令牌失败") } _, err = dao.AuthRefreshSession.Ctx(ctx).Data(do.AuthRefreshSession{SubjectId: id, Scope: "user", Terminal: terminal, Jti: claims.JTI, ExpiredAt: gtime.NewFromTimeStamp(claims.ExpireAt)}).Insert() if err != nil { return nil, 0, gerror.Wrap(err, "保存刷新令牌会话失败") } return &dto.TokenPair{AccessToken: a, RefreshToken: r, ExpiresIn: exp}, id, nil }