rbac
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// Package authsvc 是后台登录与当前用户信息的业务逻辑。
|
||||
package authsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"portal-mini-go/internal/middleware"
|
||||
"portal-mini-go/internal/model/admin"
|
||||
"portal-mini-go/internal/pkg/auth"
|
||||
"portal-mini-go/internal/pkg/errors"
|
||||
"portal-mini-go/internal/pkg/token"
|
||||
)
|
||||
|
||||
// userRepo 是登录与信息所需的用户仓储最小接口(由 repository/admin 实现)。
|
||||
type userRepo interface {
|
||||
GetByUsername(ctx context.Context, username string) (*admin.User, error)
|
||||
GetByID(ctx context.Context, id uint64) (*admin.User, error)
|
||||
}
|
||||
|
||||
// Service 后台认证业务:登录签发 JWT、当前用户信息聚合。
|
||||
type Service struct {
|
||||
userRepo userRepo
|
||||
provider middleware.PermissionProvider
|
||||
jwtSecret string
|
||||
tokenTTL time.Duration
|
||||
}
|
||||
|
||||
func New(userRepo userRepo, provider middleware.PermissionProvider, jwtSecret string, tokenTTL time.Duration) *Service {
|
||||
return &Service{userRepo: userRepo, provider: provider, jwtSecret: jwtSecret, tokenTTL: tokenTTL}
|
||||
}
|
||||
|
||||
// LoginReq 登录请求。
|
||||
type LoginReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// Login 校验账号密码,成功返回 JWT。账号不存在 / 密码错误 / 禁用均返回未授权,
|
||||
// 避免泄露账号是否存在。
|
||||
func (s *Service) Login(ctx context.Context, req LoginReq) (string, error) {
|
||||
u, err := s.userRepo.GetByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
return "", errors.ErrUnauthorized
|
||||
}
|
||||
if u.Status != admin.UserStatusEnabled {
|
||||
return "", errors.ErrUnauthorized
|
||||
}
|
||||
if !auth.VerifyPassword(req.Password, u.PasswordHash) {
|
||||
return "", errors.ErrUnauthorized
|
||||
}
|
||||
return token.Generate(s.jwtSecret, strconv.FormatUint(u.ID, 10), s.tokenTTL)
|
||||
}
|
||||
|
||||
// Info 是当前管理员聚合信息,供前端渲染与鉴权。
|
||||
type Info struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Roles []string `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Menus []admin.MenuNode `json:"menus"`
|
||||
}
|
||||
|
||||
// Info 返回当前管理员的角色、权限与可见菜单树。
|
||||
func (s *Service) Info(ctx context.Context, adminID uint64) (*Info, error) {
|
||||
u, err := s.userRepo.GetByID(ctx, adminID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles, err := s.provider.RolesOf(ctx, adminID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perms, err := s.provider.PermissionsOf(ctx, adminID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
menus, err := s.provider.MenuTree(ctx, adminID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Info{
|
||||
ID: strconv.FormatUint(u.ID, 10),
|
||||
Username: u.Username,
|
||||
Nickname: u.Nickname,
|
||||
Roles: roles,
|
||||
Permissions: perms,
|
||||
Menus: menus,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package authsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"portal-mini-go/internal/model/admin"
|
||||
"portal-mini-go/internal/pkg/auth"
|
||||
"portal-mini-go/internal/pkg/errors"
|
||||
)
|
||||
|
||||
type fakeUserRepo struct {
|
||||
byUsername map[string]*admin.User
|
||||
byID map[uint64]*admin.User
|
||||
}
|
||||
|
||||
func (r *fakeUserRepo) GetByUsername(_ context.Context, username string) (*admin.User, error) {
|
||||
if u, ok := r.byUsername[username]; ok {
|
||||
return u, nil
|
||||
}
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
func (r *fakeUserRepo) GetByID(_ context.Context, id uint64) (*admin.User, error) {
|
||||
if u, ok := r.byID[id]; ok {
|
||||
return u, nil
|
||||
}
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
type fakeProvider struct {
|
||||
roles []string
|
||||
perms []string
|
||||
menus []admin.MenuNode
|
||||
}
|
||||
|
||||
func (p *fakeProvider) Load(context.Context, uint64) ([]string, []string, error) {
|
||||
return p.roles, p.perms, nil
|
||||
}
|
||||
func (p *fakeProvider) RolesOf(context.Context, uint64) ([]string, error) { return p.roles, nil }
|
||||
func (p *fakeProvider) PermissionsOf(context.Context, uint64) ([]string, error) {
|
||||
return p.perms, nil
|
||||
}
|
||||
func (p *fakeProvider) MenuTree(context.Context, uint64) ([]admin.MenuNode, error) {
|
||||
return p.menus, nil
|
||||
}
|
||||
|
||||
func newSvc(u *admin.User) *Service {
|
||||
repo := &fakeUserRepo{
|
||||
byUsername: map[string]*admin.User{u.Username: u},
|
||||
byID: map[uint64]*admin.User{u.ID: u},
|
||||
}
|
||||
return New(repo, &fakeProvider{}, "secret", time.Hour)
|
||||
}
|
||||
|
||||
func TestLogin_Success(t *testing.T) {
|
||||
hash, _ := auth.HashPassword("Admin@123456")
|
||||
u := &admin.User{ID: 7, Username: "admin", PasswordHash: hash, Status: admin.UserStatusEnabled}
|
||||
svc := newSvc(u)
|
||||
|
||||
tok, err := svc.Login(context.Background(), LoginReq{Username: "admin", Password: "Admin@123456"})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, tok)
|
||||
}
|
||||
|
||||
func TestLogin_Failures(t *testing.T) {
|
||||
hash, _ := auth.HashPassword("Admin@123456")
|
||||
u := &admin.User{ID: 7, Username: "admin", PasswordHash: hash, Status: admin.UserStatusEnabled}
|
||||
svc := newSvc(u)
|
||||
|
||||
_, err := svc.Login(context.Background(), LoginReq{Username: "admin", Password: "wrong"})
|
||||
require.ErrorIs(t, err, errors.ErrUnauthorized)
|
||||
|
||||
disabled := &admin.User{ID: 8, Username: "disabled", PasswordHash: hash, Status: admin.UserStatusDisabled}
|
||||
svc2 := newSvc(disabled)
|
||||
_, err = svc2.Login(context.Background(), LoginReq{Username: "disabled", Password: "Admin@123456"})
|
||||
require.ErrorIs(t, err, errors.ErrUnauthorized)
|
||||
|
||||
_, err = svc.Login(context.Background(), LoginReq{Username: "nope", Password: "x"})
|
||||
require.ErrorIs(t, err, errors.ErrUnauthorized)
|
||||
}
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
hash, _ := auth.HashPassword("Admin@123456")
|
||||
u := &admin.User{ID: 7, Username: "admin", Nickname: "超管", PasswordHash: hash, Status: admin.UserStatusEnabled}
|
||||
repo := &fakeUserRepo{byID: map[uint64]*admin.User{7: u}}
|
||||
prov := &fakeProvider{roles: []string{"super_admin"}, perms: []string{"user:view"}, menus: nil}
|
||||
svc := New(repo, prov, "secret", time.Hour)
|
||||
|
||||
info, err := svc.Info(context.Background(), 7)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "7", info.ID)
|
||||
require.Equal(t, "admin", info.Username)
|
||||
require.Equal(t, []string{"super_admin"}, info.Roles)
|
||||
require.Equal(t, []string{"user:view"}, info.Permissions)
|
||||
}
|
||||
Reference in New Issue
Block a user