service.xpcool.com/internal/service/user/auth/auth.go
夏犀麟 6e3ef67709 refactor(service): 重构服务层结构并更新导入路径
- 将 admin 相关服务移动到 internal/service/admin 目录下
- 更新控制器中的服务导入路径引用
- 移除已合并的服务文件
- 添加统一工作约定文档
- 更新 API 接口定义的包路径
2026-08-26 15:40:13 +08:00

148 lines
5.4 KiB
Go
Raw 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 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 implementation not registered")
}
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, "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
}