service.xpcool.com/internal/service/user/auth/auth.go
夏犀麟 4b3c00270f
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
feat(i18n): 中文化校验提示与服务层错误信息
将 api 层校验规则提示语、service 层 gerror.Wrap 与 response.Error
错误信息、panic 未注册提示统一改为中文,并同步中文化 cmd 路由注释
与变更记录。仅涉及注释、文档与字符串改动,无业务逻辑变更。
2026-09-13 23:22:46 +08:00

148 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package user_auth
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"
)
// IUserAuth 用户端认证服务接口。
type IUserAuth interface {
Login(context.Context, dto.UserLoginInput) (*dto.TokenPair, uint64, error)
Refresh(context.Context, string) (*dto.TokenPair, uint64, error)
}
var localUserAuth IUserAuth
func UserAuth() IUserAuth {
if localUserAuth == nil {
panic("UserAuth 实现未注册")
}
return localUserAuth
}
func RegisterUserAuth(i IUserAuth) { localUserAuth = i }
// 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, "微信登录未配置")
}
openID, e := s.wechat.OpenID(ctx, in.Code)
if e != nil {
return nil, 0, gerror.Wrap(e, "微信登录失败")
}
err = dao.User.Ctx(ctx).Where(do.User{OpenId: openID}).Scan(&u)
if err != nil {
return nil, 0, gerror.Wrap(err, "查询用户失败")
}
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, "创建用户失败")
}
u.Id = uint64(id)
}
case "mobile":
// 验证码由注入的服务校验,校验成功后按手机号自动注册。
if s.sms == nil {
return nil, 0, response.Error(consts.CodeInternal, "短信登录未配置")
}
if err = s.sms.Verify(ctx, in.Mobile, in.VerifyCode); err != nil {
return nil, 0, response.Error(consts.CodeInvalidParam, "验证码错误")
}
err = dao.User.Ctx(ctx).Where(do.User{Mobile: in.Mobile}).Scan(&u)
if err != nil {
return nil, 0, gerror.Wrap(err, "查询用户失败")
}
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, "创建用户失败")
}
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, "查询用户失败")
}
if u.Id == 0 || bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(in.Password)) != nil {
return nil, 0, response.Error(consts.CodeUserPasswordWrong, "账号或密码错误")
}
default:
return nil, 0, response.Error(consts.CodeUserLoginType, "不支持的登录方式")
}
if u.Status != 1 {
return nil, 0, response.Error(consts.CodeForbidden, "用户已被禁用")
}
_, 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, "更新登录时间失败")
}
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, "刷新令牌无效")
}
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, "签发令牌失败")
}
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
}