Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
将 api 层校验规则提示语、service 层 gerror.Wrap 与 response.Error 错误信息、panic 未注册提示统一改为中文,并同步中文化 cmd 路由注释 与变更记录。仅涉及注释、文档与字符串改动,无业务逻辑变更。
148 lines
5.4 KiB
Go
148 lines
5.4 KiB
Go
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
|
||
}
|