adminLoginLogic.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. package pub
  2. import (
  3. "context"
  4. "crypto/subtle"
  5. "errors"
  6. "fmt"
  7. "time"
  8. "perms-system-server/internal/consts"
  9. authHelper "perms-system-server/internal/logic/auth"
  10. "perms-system-server/internal/middleware"
  11. "perms-system-server/internal/model/user"
  12. "perms-system-server/internal/response"
  13. "perms-system-server/internal/svc"
  14. "perms-system-server/internal/types"
  15. "github.com/zeromicro/go-zero/core/limit"
  16. "github.com/zeromicro/go-zero/core/logx"
  17. "golang.org/x/crypto/bcrypt"
  18. )
  19. type AdminLoginLogic struct {
  20. logx.Logger
  21. ctx context.Context
  22. svcCtx *svc.ServiceContext
  23. }
  24. func NewAdminLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminLoginLogic {
  25. return &AdminLoginLogic{
  26. Logger: logx.WithContext(ctx),
  27. ctx: ctx,
  28. svcCtx: svcCtx,
  29. }
  30. }
  31. // AdminLogin 管理后台登录。仅限超级管理员通过 managementKey + 用户名密码登录管理后台,返回 JWT 令牌对。
  32. func (l *AdminLoginLogic) AdminLogin(req *types.AdminLoginReq) (resp *types.LoginResp, err error) {
  33. if subtle.ConstantTimeCompare([]byte(req.ManagementKey), []byte(l.svcCtx.Config.Auth.ManagementKey)) != 1 {
  34. return nil, response.ErrUnauthorized("managementKey无效")
  35. }
  36. // 限流 key 使用 "admin:<clientIP>:<username>" 双键维度:只有来自同一 IP + 同一 username 的
  37. // 连续失败才会累加计数,远端任意 IP 无法通过只打 username 把任意超管账号永久锁死(见审计 H-1)。
  38. // clientIP 由 AdminLoginRateLimit 中间件注入 context;解析失败时用 "unknown" 走共享桶兜底,
  39. // 仍有 5min/10 次的总体上限,避免退化为"无限流"。
  40. if l.svcCtx.UsernameLoginLimit != nil {
  41. clientIP := middleware.GetClientIP(l.ctx)
  42. if clientIP == "" {
  43. clientIP = "unknown"
  44. }
  45. key := fmt.Sprintf("admin:%s:%s", clientIP, req.Username)
  46. code, _ := l.svcCtx.UsernameLoginLimit.Take(key)
  47. if code == limit.OverQuota {
  48. return nil, response.NewCodeError(429, "登录尝试过于频繁,请5分钟后再试")
  49. }
  50. }
  51. u, err := l.svcCtx.SysUserModel.FindOneByUsername(l.ctx, req.Username)
  52. if err != nil {
  53. if errors.Is(err, user.ErrNotFound) {
  54. bcrypt.CompareHashAndPassword(dummyBcryptHash, []byte(req.Password))
  55. return nil, response.ErrUnauthorized("用户名或密码错误")
  56. }
  57. return nil, err
  58. }
  59. // 审计 L-N3:把 IsSuperAdmin 判断前置到真 bcrypt 之前,防止"存在但非超管"分支因跳过真 bcrypt
  60. // 比"存在是超管且密码错"分支快一段时间(数十 ms),让攻击者借耗时差筛出"存在的超管账号"。
  61. // 这里仍然要走一次 dummyBcryptHash,把时序抹平到与"真 bcrypt 失败 + 返回 ErrUnauthorized"同阶。
  62. if u.IsSuperAdmin != consts.IsSuperAdminYes {
  63. bcrypt.CompareHashAndPassword(dummyBcryptHash, []byte(req.Password))
  64. return nil, response.ErrUnauthorized("用户名或密码错误")
  65. }
  66. if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(req.Password)); err != nil {
  67. return nil, response.ErrUnauthorized("用户名或密码错误")
  68. }
  69. if u.Status != consts.StatusEnabled {
  70. return nil, response.ErrUnauthorized("用户名或密码错误")
  71. }
  72. ud, err := l.svcCtx.UserDetailsLoader.Load(l.ctx, u.Id, "")
  73. if err != nil {
  74. return nil, response.NewCodeError(503, "服务暂时不可用,请稍后重试")
  75. }
  76. accessToken, err := authHelper.GenerateAccessToken(
  77. l.svcCtx.Config.Auth.AccessSecret,
  78. l.svcCtx.Config.Auth.AccessExpire,
  79. ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.TokenVersion,
  80. )
  81. if err != nil {
  82. return nil, err
  83. }
  84. refreshToken, err := authHelper.GenerateRefreshToken(
  85. l.svcCtx.Config.Auth.RefreshSecret,
  86. l.svcCtx.Config.Auth.RefreshExpire,
  87. ud.UserId, ud.ProductCode, ud.TokenVersion,
  88. )
  89. if err != nil {
  90. return nil, err
  91. }
  92. return &types.LoginResp{
  93. AccessToken: accessToken,
  94. RefreshToken: refreshToken,
  95. Expires: time.Now().Unix() + l.svcCtx.Config.Auth.AccessExpire,
  96. UserInfo: types.UserInfo{
  97. UserId: ud.UserId,
  98. Username: ud.Username,
  99. Nickname: ud.Nickname,
  100. Avatar: ud.Avatar,
  101. Email: ud.Email,
  102. Phone: ud.Phone,
  103. IsSuperAdmin: ud.IsSuperAdminRaw,
  104. MustChangePassword: ud.MustChangePwdRaw,
  105. MemberType: ud.MemberType,
  106. Perms: ud.Perms,
  107. },
  108. }, nil
  109. }