package service import ( "context" "regexp" "strings" "github.com/gogf/gf/v2/errors/gerror" "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" ) type adminAuth struct{ tokens *jwt.Service } func NewAdminAuth(tokens *jwt.Service) IAdminAuth { return &adminAuth{tokens} } func (s *adminAuth) Login(ctx context.Context, in dto.AdminLoginInput) (*dto.TokenPair, uint64, error) { // 管理端只允许账号密码登录,状态异常或密码错误均返回统一错误,避免枚举账号。 var a entity.AdminUser if err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Scan(&a); err != nil { return nil, 0, gerror.Wrap(err, "query administrator") } if a.Id == 0 { return nil, 0, response.Error(consts.CodeAdminNotFound, "administrator not found") } if a.Status != 1 || bcrypt.CompareHashAndPassword([]byte(a.PasswordHash), []byte(in.Password)) != nil { return nil, 0, response.Error(consts.CodeAdminPasswordWrong, "username or password incorrect") } access, refresh, exp, err := s.tokens.Issue(a.Id, "admin", "") if err != nil { return nil, 0, gerror.Wrap(err, "issue token") } return &dto.TokenPair{AccessToken: access, RefreshToken: refresh, ExpiresIn: exp}, a.Id, nil } func (s *adminAuth) HasPermission(ctx context.Context, adminID uint64, permission string) (bool, error) { // 多角色权限通过管理员-角色-菜单三表关联查询,菜单中的 permission 即接口权限标识。 count, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role_menu rm", "ur.role_id=rm.role_id").LeftJoin("admin_menu m", "rm.menu_id=m.id").Where("ur.admin_user_id", adminID).Where("m.permission", permission).Where("m.status", 1).Count() if err != nil { return false, gerror.Wrap(err, "check permission") } return count > 0, nil } func (s *adminAuth) Info(ctx context.Context, adminID uint64) (*dto.AdminInfo, error) { var a entity.AdminUser if err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: adminID}).Scan(&a); err != nil { return nil, gerror.Wrap(err, "query admin info") } if a.Id == 0 { return nil, response.Error(consts.CodeAdminNotFound, "administrator not found") } roles, err := s.roleCodes(ctx, adminID) if err != nil { return nil, err } return &dto.AdminInfo{AdminID: a.Id, Username: a.Username, Nickname: a.Nickname, Roles: roles}, nil } func (s *adminAuth) Codes(ctx context.Context, adminID uint64) ([]string, error) { // 权限码 = 该管理员所有启用角色绑定的菜单 permission(含菜单与按钮级), // 同时用作 vben 前端按钮权限码与后端 X-Permission 校验标识。 list, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role_menu rm", "ur.role_id=rm.role_id").LeftJoin("admin_menu m", "rm.menu_id=m.id").Where("ur.admin_user_id", adminID).Where("m.status", 1).WhereGT("m.permission", "").Fields("DISTINCT m.permission").Array() if err != nil { return nil, gerror.Wrap(err, "query access codes") } codes := make([]string, 0, len(list)) for _, v := range list { codes = append(codes, v.String()) } return codes, nil } func (s *adminAuth) roleCodes(ctx context.Context, adminID uint64) ([]string, error) { list, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role r", "ur.role_id=r.id").Where("ur.admin_user_id", adminID).Where("r.status", 1).Fields("DISTINCT r.code").Array() if err != nil { return nil, gerror.Wrap(err, "query admin roles") } codes := make([]string, 0, len(list)) for _, v := range list { codes = append(codes, v.String()) } return codes, nil } func (s *adminAuth) PermissionForPath(ctx context.Context, method, path string) (string, error) { // 从菜单表 type=2(按钮/API)行反查当前请求所需的权限码。 // 未配置映射的接口一律拒绝访问(返回空则中间件拦截)。 var list []entity.AdminMenu if err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Type: 2, Status: 1}).Scan(&list); err != nil { return "", gerror.Wrap(err, "query permission mappings") } req := method + " " + path for _, m := range list { if matchRoute(m.Path, req) { return m.Permission, nil } } return "", nil } // matchRoute 匹配 "METHOD /path" 模式,{id} 视为动态段。 func matchRoute(pattern, req string) bool { if pattern == "" { return false } var b strings.Builder b.WriteByte('^') for i := 0; i < len(pattern); i++ { c := pattern[i] switch { case c == '{': if j := strings.IndexByte(pattern[i:], '}'); j > 0 { b.WriteString("[^/]+") i += j } else { b.WriteString(regexp.QuoteMeta(string(c))) } case c == ' ' || c == '/' || c == '-' || c == '_' || c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'): b.WriteByte(c) default: b.WriteString(regexp.QuoteMeta(string(c))) } } b.WriteByte('$') ok, err := regexp.MatchString(b.String(), req) return err == nil && ok }