Jelajahi Sumber

first commit

BaiLuoYan 1 Minggu lalu
melakukan
fdbca914c1
16 mengubah file dengan 2175 tambahan dan 0 penghapusan
  1. 0 0
      README.md
  2. 45 0
      cache.go
  3. 58 0
      client.go
  4. 68 0
      collector.go
  5. 55 0
      config.go
  6. 24 0
      context.go
  7. 167 0
      example/main.go
  8. 124 0
      fieldperm.go
  9. 17 0
      go.mod
  10. 42 0
      go.sum
  11. 174 0
      middleware.go
  12. 867 0
      pb/perm.pb.go
  13. 85 0
      pb/perm.proto
  14. 273 0
      pb/perm_grpc.pb.go
  15. 159 0
      permlib.go
  16. 17 0
      sync.go

+ 0 - 0
README.md


+ 45 - 0
cache.go

@@ -0,0 +1,45 @@
+package permlib
+
+import (
+	"sync"
+	"time"
+
+	"golang.org/x/sync/singleflight"
+)
+
+type cachedUser struct {
+	UserId      int64
+	Username    string
+	ProductCode string
+	MemberType  string
+	Perms       []string
+	expiresAt   time.Time
+}
+
+type permCache struct {
+	store sync.Map
+	ttl   time.Duration
+	sf    singleflight.Group
+}
+
+func newPermCache(ttl time.Duration) *permCache {
+	return &permCache{ttl: ttl}
+}
+
+func (c *permCache) get(key string) (*cachedUser, bool) {
+	v, ok := c.store.Load(key)
+	if !ok {
+		return nil, false
+	}
+	entry := v.(*cachedUser)
+	if time.Now().After(entry.expiresAt) {
+		c.store.Delete(key)
+		return nil, false
+	}
+	return entry, true
+}
+
+func (c *permCache) set(key string, u *cachedUser) {
+	u.expiresAt = time.Now().Add(c.ttl)
+	c.store.Store(key, u)
+}

+ 58 - 0
client.go

@@ -0,0 +1,58 @@
+package permlib
+
+import (
+	"context"
+
+	"github.com/xiaozi/permlib/pb"
+
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+)
+
+type grpcClient struct {
+	cli pb.PermServiceClient
+}
+
+func newGRPCClient(target string) (*grpcClient, error) {
+	conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		return nil, err
+	}
+	return &grpcClient{cli: pb.NewPermServiceClient(conn)}, nil
+}
+
+func (c *grpcClient) verifyToken(ctx context.Context, accessToken string) (*pb.VerifyTokenResp, error) {
+	return c.cli.VerifyToken(ctx, &pb.VerifyTokenReq{AccessToken: accessToken})
+}
+
+func (c *grpcClient) login(ctx context.Context, productCode, username, password string) (*pb.LoginResp, error) {
+	return c.cli.Login(ctx, &pb.LoginReq{
+		ProductCode: productCode,
+		Username:    username,
+		Password:    password,
+	})
+}
+
+func (c *grpcClient) refreshToken(ctx context.Context, refreshToken, productCode string) (*pb.RefreshTokenResp, error) {
+	return c.cli.RefreshToken(ctx, &pb.RefreshTokenReq{
+		RefreshToken: refreshToken,
+		ProductCode:  productCode,
+	})
+}
+
+func (c *grpcClient) syncPermissions(ctx context.Context, appKey, appSecret string, perms []*pb.PermItem) (*pb.SyncPermissionsResp, error) {
+	return c.cli.SyncPermissions(ctx, &pb.SyncPermissionsReq{
+		AppKey:    appKey,
+		AppSecret: appSecret,
+		Perms:     perms,
+	})
+}
+
+func (c *grpcClient) getUserPerms(ctx context.Context, appKey, appSecret string, userId int64, productCode string) (*pb.GetUserPermsResp, error) {
+	return c.cli.GetUserPerms(ctx, &pb.GetUserPermsReq{
+		AppKey:      appKey,
+		AppSecret:   appSecret,
+		UserId:      userId,
+		ProductCode: productCode,
+	})
+}

+ 68 - 0
collector.go

@@ -0,0 +1,68 @@
+package permlib
+
+import (
+	"strings"
+
+	"github.com/xiaozi/permlib/pb"
+)
+
+func (e *Engine) collectPerms() []*pb.PermItem {
+	seen := make(map[string]bool)
+	var perms []*pb.PermItem
+
+	add := func(code, name string) {
+		if seen[code] {
+			return
+		}
+		seen[code] = true
+		if name == "" {
+			name = generatePermName(code)
+		}
+		perms = append(perms, &pb.PermItem{Code: code, Name: name})
+	}
+
+	for _, decl := range e.staticPerms {
+		add(decl.Code, decl.Name)
+		dataCode := apiToDataCode(decl.Code)
+		if dataCode != "" {
+			add(dataCode, "")
+		}
+	}
+
+	return perms
+}
+
+func apiToDataCode(apiCode string) string {
+	if !strings.HasPrefix(apiCode, "api:") {
+		return ""
+	}
+	return "data:" + apiCode[4:]
+}
+
+func generatePermName(code string) string {
+	parts := strings.Split(code, ":")
+	if len(parts) < 2 {
+		return code
+	}
+
+	nameMap := map[string]string{
+		"read":   "读取",
+		"write":  "写入",
+		"create": "创建",
+		"update": "更新",
+		"delete": "删除",
+		"list":   "列表",
+		"detail": "详情",
+	}
+
+	var result []string
+	for i := 1; i < len(parts); i++ {
+		p := parts[i]
+		if mapped, ok := nameMap[p]; ok {
+			result = append(result, mapped)
+		} else {
+			result = append(result, p)
+		}
+	}
+	return strings.Join(result, "-")
+}

+ 55 - 0
config.go

@@ -0,0 +1,55 @@
+package permlib
+
+import (
+	"errors"
+	"net/http"
+	"time"
+)
+
+type FieldWriteMode int
+
+const (
+	FieldWriteSilent FieldWriteMode = iota
+	FieldWriteReject
+)
+
+// AuthCallbacks 鉴权回调,调用方必须提供,permlib 不内置任何响应格式
+type AuthCallbacks struct {
+	// OnError 鉴权失败时调用
+	// httpStatus: 建议的 HTTP 状态码(401/403/500)
+	// code: 业务错误码
+	// msg: 错误信息
+	OnError func(w http.ResponseWriter, r *http.Request, httpStatus int, code int, msg string)
+
+	// OnSuccess 鉴权成功时调用
+	// user: 当前用户信息(含 Perms)
+	// hasDataPerm: 当前请求是否有对应的 data:* 数据权限
+	// next: 业务 handler,hasDataPerm 为 true 时调用,否则返回空数据
+	OnSuccess func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc, user *UserInfo, hasDataPerm bool)
+}
+
+type Config struct {
+	ProductCode    string
+	AppKey         string
+	AppSecret      string
+	PermServerAddr string
+	FieldWriteMode FieldWriteMode
+	CacheTTL       time.Duration
+	Callbacks      AuthCallbacks
+}
+
+func (c *Config) validate() error {
+	if c.Callbacks.OnError == nil {
+		return errors.New("Config.Callbacks.OnError is required")
+	}
+	if c.Callbacks.OnSuccess == nil {
+		return errors.New("Config.Callbacks.OnSuccess is required")
+	}
+	return nil
+}
+
+func (c *Config) defaults() {
+	if c.CacheTTL == 0 {
+		c.CacheTTL = 5 * time.Minute
+	}
+}

+ 24 - 0
context.go

@@ -0,0 +1,24 @@
+package permlib
+
+import "context"
+
+type contextKey string
+
+const ctxKeyUser contextKey = "permlib_user"
+
+type UserInfo struct {
+	UserId      int64
+	Username    string
+	ProductCode string
+	MemberType  string
+	Perms       []string
+}
+
+func GetUser(ctx context.Context) *UserInfo {
+	v, _ := ctx.Value(ctxKeyUser).(*UserInfo)
+	return v
+}
+
+func withUser(ctx context.Context, u *UserInfo) context.Context {
+	return context.WithValue(ctx, ctxKeyUser, u)
+}

+ 167 - 0
example/main.go

@@ -0,0 +1,167 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"io"
+	"log"
+	"net/http"
+
+	"github.com/xiaozi/permlib"
+)
+
+type CreateUserReq struct {
+	Username string `json:"username"`
+	Email    string `json:"email"  perm:"data:user:email:write"`
+	Phone    string `json:"phone"  perm:"data:user:phone:write"`
+}
+
+type UserListResp struct {
+	ID       int64  `json:"id"`
+	Username string `json:"username"`
+	Email    string `json:"email"  perm:"data:user:email:read"`
+	Phone    string `json:"phone"  perm:"data:user:phone:read"`
+}
+
+func writeJSON(w http.ResponseWriter, status int, body interface{}) {
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	w.WriteHeader(status)
+	json.NewEncoder(w).Encode(body)
+}
+
+func main() {
+	engine, err := permlib.New(permlib.Config{
+		ProductCode:    "crm",
+		AppKey:         "ak_crm",
+		AppSecret:      "sk_crm",
+		PermServerAddr: "localhost:10002",
+		FieldWriteMode: permlib.FieldWriteSilent,
+		Callbacks: permlib.AuthCallbacks{
+			OnError: func(w http.ResponseWriter, r *http.Request, httpStatus int, code int, msg string) {
+				writeJSON(w, httpStatus, map[string]interface{}{
+					"success":      false,
+					"errorCode":    code,
+					"errorMessage": msg,
+				})
+			},
+			OnSuccess: func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc, user *permlib.UserInfo, hasDataPerm bool) {
+				if !hasDataPerm {
+					writeJSON(w, http.StatusOK, map[string]interface{}{
+						"success": true,
+						"data":    map[string]interface{}{},
+					})
+					return
+				}
+				next(w, r)
+			},
+		},
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	if err := engine.Start(context.Background()); err != nil {
+		log.Fatal(err)
+	}
+
+	mux := http.NewServeMux()
+
+	// 公开接口:产品端自己实现 handler,完全控制响应格式
+	mux.HandleFunc("POST /api/auth/login", makeLoginHandler(engine))
+	mux.HandleFunc("POST /api/auth/refreshToken", makeRefreshTokenHandler(engine))
+
+	// 鉴权接口:通过 AuthMiddlewareWithOpts 套上鉴权 + 字段过滤
+	mux.HandleFunc("POST /api/user/create", engine.AuthMiddleware(createUser))
+	mux.HandleFunc("POST /api/user/list", engine.AuthMiddleware(listUsers))
+
+	log.Println("server listening on :8080")
+	log.Fatal(http.ListenAndServe(":8080", mux))
+}
+
+func makeLoginHandler(engine *permlib.Engine) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		body, err := io.ReadAll(r.Body)
+		r.Body.Close()
+		if err != nil {
+			writeJSON(w, http.StatusBadRequest, map[string]interface{}{"success": false, "errorMessage": "请求体读取失败"})
+			return
+		}
+		var req struct {
+			Username string `json:"username"`
+			Password string `json:"password"`
+		}
+		if err := json.Unmarshal(body, &req); err != nil || req.Username == "" || req.Password == "" {
+			writeJSON(w, http.StatusBadRequest, map[string]interface{}{"success": false, "errorMessage": "用户名和密码不能为空"})
+			return
+		}
+		result, err := engine.Login(r.Context(), req.Username, req.Password)
+		if err != nil {
+			writeJSON(w, http.StatusUnauthorized, map[string]interface{}{"success": false, "errorMessage": err.Error()})
+			return
+		}
+		writeJSON(w, http.StatusOK, map[string]interface{}{
+			"success": true,
+			"data": map[string]interface{}{
+				"accessToken":  result.AccessToken,
+				"refreshToken": result.RefreshToken,
+				"expires":      result.Expires,
+				"userId":       result.UserId,
+				"username":     result.Username,
+				"nickname":     result.Nickname,
+				"memberType":   result.MemberType,
+				"perms":        result.Perms,
+			},
+		})
+	}
+}
+
+func makeRefreshTokenHandler(engine *permlib.Engine) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		body, err := io.ReadAll(r.Body)
+		r.Body.Close()
+		if err != nil {
+			writeJSON(w, http.StatusBadRequest, map[string]interface{}{"success": false, "errorMessage": "请求体读取失败"})
+			return
+		}
+		var req struct {
+			RefreshToken string `json:"refreshToken"`
+		}
+		if err := json.Unmarshal(body, &req); err != nil || req.RefreshToken == "" {
+			writeJSON(w, http.StatusBadRequest, map[string]interface{}{"success": false, "errorMessage": "refreshToken不能为空"})
+			return
+		}
+		result, err := engine.RefreshToken(r.Context(), req.RefreshToken)
+		if err != nil {
+			writeJSON(w, http.StatusUnauthorized, map[string]interface{}{"success": false, "errorMessage": err.Error()})
+			return
+		}
+		writeJSON(w, http.StatusOK, map[string]interface{}{
+			"success": true,
+			"data": map[string]interface{}{
+				"accessToken":  result.AccessToken,
+				"refreshToken": result.RefreshToken,
+				"expires":      result.Expires,
+				"perms":        result.Perms,
+			},
+		})
+	}
+}
+
+func createUser(w http.ResponseWriter, r *http.Request) {
+	user := permlib.GetUser(r.Context())
+	writeJSON(w, http.StatusOK, map[string]interface{}{
+		"success": true,
+		"data":    map[string]interface{}{"createdBy": user.Username},
+	})
+}
+
+func listUsers(w http.ResponseWriter, r *http.Request) {
+	users := []UserListResp{
+		{ID: 1, Username: "alice", Email: "alice@example.com", Phone: "13800001111"},
+		{ID: 2, Username: "bob", Email: "bob@example.com", Phone: "13800002222"},
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{
+		"success": true,
+		"data":    users,
+	})
+}

+ 124 - 0
fieldperm.go

@@ -0,0 +1,124 @@
+package permlib
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"net/http"
+)
+
+type fieldPermEntry struct {
+	jsonName string
+	permCode string
+}
+
+type filterResponseWriter struct {
+	http.ResponseWriter
+	buf    bytes.Buffer
+	req    *http.Request
+	perms  []string
+	engine *Engine
+	code   int
+}
+
+func newFilterResponseWriter(w http.ResponseWriter, req *http.Request, perms []string, e *Engine) *filterResponseWriter {
+	return &filterResponseWriter{
+		ResponseWriter: w,
+		req:            req,
+		perms:          perms,
+		engine:         e,
+		code:           200,
+	}
+}
+
+func (fw *filterResponseWriter) WriteHeader(code int) {
+	fw.code = code
+}
+
+func (fw *filterResponseWriter) Write(b []byte) (int, error) {
+	return fw.buf.Write(b)
+}
+
+func (fw *filterResponseWriter) flush() {
+	body := fw.buf.Bytes()
+
+	if len(body) > 0 {
+		body = fw.engine.filterResponseBody(body, fw.req, fw.perms)
+	}
+
+	fw.ResponseWriter.WriteHeader(fw.code)
+	fw.ResponseWriter.Write(body)
+}
+
+func (e *Engine) filterResponseBody(body []byte, req *http.Request, perms []string) []byte {
+	key := req.Method + " " + req.URL.Path
+	if fm, ok := e.fieldPerms[key]; ok && len(fm.Response) > 0 {
+		return filterResponseByMap(body, fm.Response, perms)
+	}
+	return body
+}
+
+func filterResponseByMap(body []byte, fieldMap map[string]string, perms []string) []byte {
+	permSet := toPermSet(perms)
+	body = bytes.TrimSpace(body)
+	if len(body) == 0 {
+		return body
+	}
+
+	entries := make([]fieldPermEntry, 0, len(fieldMap))
+	for jsonField, permCode := range fieldMap {
+		entries = append(entries, fieldPermEntry{jsonName: jsonField, permCode: permCode})
+	}
+
+	if body[0] == '[' {
+		var arr []json.RawMessage
+		if err := json.Unmarshal(body, &arr); err != nil {
+			return body
+		}
+		for i, item := range arr {
+			arr[i] = filterObject(item, entries, permSet)
+		}
+		result, _ := json.Marshal(arr)
+		return result
+	}
+	return filterObject(body, entries, permSet)
+}
+
+func filterObject(raw json.RawMessage, entries []fieldPermEntry, permSet map[string]bool) json.RawMessage {
+	var obj map[string]json.RawMessage
+	if err := json.Unmarshal(raw, &obj); err != nil {
+		return raw
+	}
+	for _, entry := range entries {
+		if _, has := obj[entry.jsonName]; has && !permSet[entry.permCode] {
+			delete(obj, entry.jsonName)
+		}
+	}
+	result, _ := json.Marshal(obj)
+	return result
+}
+
+func toPermSet(perms []string) map[string]bool {
+	m := make(map[string]bool, len(perms))
+	for _, p := range perms {
+		m[p] = true
+	}
+	return m
+}
+
+func readBody(req *http.Request) ([]byte, error) {
+	if req.Body == nil {
+		return nil, nil
+	}
+	body, err := io.ReadAll(req.Body)
+	req.Body.Close()
+	if err != nil {
+		return nil, err
+	}
+	req.Body = io.NopCloser(bytes.NewReader(body))
+	return body, nil
+}
+
+func restoreBody(req *http.Request, body []byte) {
+	req.Body = io.NopCloser(bytes.NewReader(body))
+}

+ 17 - 0
go.mod

@@ -0,0 +1,17 @@
+module github.com/xiaozi/permlib
+
+go 1.25.0
+
+require (
+	github.com/golang-jwt/jwt/v4 v4.5.2
+	golang.org/x/sync v0.20.0
+	google.golang.org/grpc v1.79.3
+	google.golang.org/protobuf v1.36.11
+)
+
+require (
+	golang.org/x/net v0.48.0 // indirect
+	golang.org/x/sys v0.39.0 // indirect
+	golang.org/x/text v0.32.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
+)

+ 42 - 0
go.sum

@@ -0,0 +1,42 @@
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
+go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
+go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
+go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
+go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
+go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
+google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
+google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=

+ 174 - 0
middleware.go

@@ -0,0 +1,174 @@
+package permlib
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+)
+
+func (e *Engine) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, req *http.Request) {
+		authHeader := req.Header.Get("Authorization")
+		if authHeader == "" {
+			e.cfg.Callbacks.OnError(w, req, 401, 401, "未登录")
+			return
+		}
+
+		tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
+		if tokenStr == authHeader {
+			e.cfg.Callbacks.OnError(w, req, 401, 401, "token格式错误")
+			return
+		}
+
+		info, err := e.verifyAndGetUser(req.Context(), tokenStr)
+		if err != nil {
+			e.cfg.Callbacks.OnError(w, req, 401, 401, "token无效或已过期")
+			return
+		}
+
+		apiCode := e.resolvePermCode(req)
+		if apiCode != "" && !containsPerm(info.Perms, apiCode) {
+			e.cfg.Callbacks.OnError(w, req, 403, 403, fmt.Sprintf("无权访问: %s", apiCode))
+			return
+		}
+
+		user := &UserInfo{
+			UserId:      info.UserId,
+			Username:    info.Username,
+			ProductCode: info.ProductCode,
+			MemberType:  info.MemberType,
+			Perms:       info.Perms,
+		}
+		ctx := withUser(req.Context(), user)
+
+		req, rejected := e.filterRequest(req, info.Perms)
+		if rejected {
+			e.cfg.Callbacks.OnError(w, req, 403, 403, "包含无权写入的字段")
+			return
+		}
+
+		dataCode := e.resolveDataCode(req, apiCode)
+		hasDataPerm := dataCode == "" || containsPerm(info.Perms, dataCode)
+
+		var wrappedNext http.HandlerFunc
+		if e.hasRespFilter(req) {
+			wrappedNext = func(w http.ResponseWriter, req *http.Request) {
+				rw := newFilterResponseWriter(w, req, info.Perms, e)
+				next(rw, req)
+				rw.flush()
+			}
+		} else {
+			wrappedNext = next
+		}
+
+		e.cfg.Callbacks.OnSuccess(w, req.WithContext(ctx), wrappedNext, user, hasDataPerm)
+	}
+}
+
+func (e *Engine) resolvePermCode(req *http.Request) string {
+	key := req.Method + " " + req.URL.Path
+	if decl, ok := e.routePerms[key]; ok {
+		return decl.PermCode
+	}
+	return ""
+}
+
+func (e *Engine) resolveDataCode(req *http.Request, apiCode string) string {
+	key := req.Method + " " + req.URL.Path
+	if decl, ok := e.routePerms[key]; ok {
+		return decl.DataCode
+	}
+	if apiCode != "" {
+		return apiToDataCode(apiCode)
+	}
+	return ""
+}
+
+func (e *Engine) hasRespFilter(req *http.Request) bool {
+	key := req.Method + " " + req.URL.Path
+	if fm, ok := e.fieldPerms[key]; ok {
+		return len(fm.Response) > 0
+	}
+	return false
+}
+
+func (e *Engine) filterRequest(req *http.Request, perms []string) (*http.Request, bool) {
+	key := req.Method + " " + req.URL.Path
+	if fm, ok := e.fieldPerms[key]; ok && len(fm.Request) > 0 {
+		return filterRequestByMap(req, fm.Request, perms, e.cfg.FieldWriteMode)
+	}
+	return req, false
+}
+
+func (e *Engine) verifyAndGetUser(ctx context.Context, token string) (*cachedUser, error) {
+	if u, ok := e.cache.get(token); ok {
+		return u, nil
+	}
+
+	v, err, _ := e.cache.sf.Do(token, func() (interface{}, error) {
+		resp, err := e.client.verifyToken(ctx, token)
+		if err != nil {
+			return nil, err
+		}
+		if !resp.Valid {
+			return nil, fmt.Errorf("token验证失败")
+		}
+		u := &cachedUser{
+			UserId:      resp.UserId,
+			Username:    resp.Username,
+			ProductCode: resp.ProductCode,
+			MemberType:  resp.MemberType,
+			Perms:       resp.Perms,
+		}
+		e.cache.set(token, u)
+		return u, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	return v.(*cachedUser), nil
+}
+
+func containsPerm(perms []string, code string) bool {
+	for _, p := range perms {
+		if p == code {
+			return true
+		}
+	}
+	return false
+}
+
+func filterRequestByMap(req *http.Request, fieldMap map[string]string, perms []string, mode FieldWriteMode) (*http.Request, bool) {
+	if req.Body == nil {
+		return req, false
+	}
+
+	body, err := readBody(req)
+	if err != nil || len(body) == 0 {
+		return req, false
+	}
+
+	var obj map[string]json.RawMessage
+	if err := json.Unmarshal(body, &obj); err != nil {
+		restoreBody(req, body)
+		return req, false
+	}
+
+	permSet := toPermSet(perms)
+	for jsonField, permCode := range fieldMap {
+		if _, has := obj[jsonField]; has && !permSet[permCode] {
+			if mode == FieldWriteReject {
+				restoreBody(req, body)
+				return req, true
+			}
+			delete(obj, jsonField)
+		}
+	}
+
+	filtered, _ := json.Marshal(obj)
+	restoreBody(req, filtered)
+	req.ContentLength = int64(len(filtered))
+	return req, false
+}

+ 867 - 0
pb/perm.pb.go

@@ -0,0 +1,867 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.11
+// 	protoc        v7.34.1
+// source: pb/perm.proto
+
+package pb
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type PermItem struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Code          string                 `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
+	Name          string                 `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+	Remark        string                 `protobuf:"bytes,3,opt,name=remark,proto3" json:"remark,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *PermItem) Reset() {
+	*x = PermItem{}
+	mi := &file_pb_perm_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *PermItem) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PermItem) ProtoMessage() {}
+
+func (x *PermItem) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PermItem.ProtoReflect.Descriptor instead.
+func (*PermItem) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *PermItem) GetCode() string {
+	if x != nil {
+		return x.Code
+	}
+	return ""
+}
+
+func (x *PermItem) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *PermItem) GetRemark() string {
+	if x != nil {
+		return x.Remark
+	}
+	return ""
+}
+
+type SyncPermissionsReq struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	AppKey        string                 `protobuf:"bytes,1,opt,name=appKey,proto3" json:"appKey,omitempty"`
+	AppSecret     string                 `protobuf:"bytes,2,opt,name=appSecret,proto3" json:"appSecret,omitempty"`
+	Perms         []*PermItem            `protobuf:"bytes,3,rep,name=perms,proto3" json:"perms,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SyncPermissionsReq) Reset() {
+	*x = SyncPermissionsReq{}
+	mi := &file_pb_perm_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SyncPermissionsReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SyncPermissionsReq) ProtoMessage() {}
+
+func (x *SyncPermissionsReq) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SyncPermissionsReq.ProtoReflect.Descriptor instead.
+func (*SyncPermissionsReq) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *SyncPermissionsReq) GetAppKey() string {
+	if x != nil {
+		return x.AppKey
+	}
+	return ""
+}
+
+func (x *SyncPermissionsReq) GetAppSecret() string {
+	if x != nil {
+		return x.AppSecret
+	}
+	return ""
+}
+
+func (x *SyncPermissionsReq) GetPerms() []*PermItem {
+	if x != nil {
+		return x.Perms
+	}
+	return nil
+}
+
+type SyncPermissionsResp struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Added         int64                  `protobuf:"varint,1,opt,name=added,proto3" json:"added,omitempty"`
+	Updated       int64                  `protobuf:"varint,2,opt,name=updated,proto3" json:"updated,omitempty"`
+	Disabled      int64                  `protobuf:"varint,3,opt,name=disabled,proto3" json:"disabled,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SyncPermissionsResp) Reset() {
+	*x = SyncPermissionsResp{}
+	mi := &file_pb_perm_proto_msgTypes[2]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SyncPermissionsResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SyncPermissionsResp) ProtoMessage() {}
+
+func (x *SyncPermissionsResp) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[2]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SyncPermissionsResp.ProtoReflect.Descriptor instead.
+func (*SyncPermissionsResp) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *SyncPermissionsResp) GetAdded() int64 {
+	if x != nil {
+		return x.Added
+	}
+	return 0
+}
+
+func (x *SyncPermissionsResp) GetUpdated() int64 {
+	if x != nil {
+		return x.Updated
+	}
+	return 0
+}
+
+func (x *SyncPermissionsResp) GetDisabled() int64 {
+	if x != nil {
+		return x.Disabled
+	}
+	return 0
+}
+
+type LoginReq struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ProductCode   string                 `protobuf:"bytes,1,opt,name=productCode,proto3" json:"productCode,omitempty"`
+	Username      string                 `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
+	Password      string                 `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *LoginReq) Reset() {
+	*x = LoginReq{}
+	mi := &file_pb_perm_proto_msgTypes[3]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LoginReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LoginReq) ProtoMessage() {}
+
+func (x *LoginReq) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[3]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use LoginReq.ProtoReflect.Descriptor instead.
+func (*LoginReq) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *LoginReq) GetProductCode() string {
+	if x != nil {
+		return x.ProductCode
+	}
+	return ""
+}
+
+func (x *LoginReq) GetUsername() string {
+	if x != nil {
+		return x.Username
+	}
+	return ""
+}
+
+func (x *LoginReq) GetPassword() string {
+	if x != nil {
+		return x.Password
+	}
+	return ""
+}
+
+type LoginResp struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	AccessToken   string                 `protobuf:"bytes,1,opt,name=accessToken,proto3" json:"accessToken,omitempty"`
+	RefreshToken  string                 `protobuf:"bytes,2,opt,name=refreshToken,proto3" json:"refreshToken,omitempty"`
+	Expires       int64                  `protobuf:"varint,3,opt,name=expires,proto3" json:"expires,omitempty"`
+	UserId        int64                  `protobuf:"varint,4,opt,name=userId,proto3" json:"userId,omitempty"`
+	Username      string                 `protobuf:"bytes,5,opt,name=username,proto3" json:"username,omitempty"`
+	Nickname      string                 `protobuf:"bytes,6,opt,name=nickname,proto3" json:"nickname,omitempty"`
+	MemberType    string                 `protobuf:"bytes,7,opt,name=memberType,proto3" json:"memberType,omitempty"`
+	Perms         []string               `protobuf:"bytes,8,rep,name=perms,proto3" json:"perms,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *LoginResp) Reset() {
+	*x = LoginResp{}
+	mi := &file_pb_perm_proto_msgTypes[4]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LoginResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LoginResp) ProtoMessage() {}
+
+func (x *LoginResp) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[4]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use LoginResp.ProtoReflect.Descriptor instead.
+func (*LoginResp) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *LoginResp) GetAccessToken() string {
+	if x != nil {
+		return x.AccessToken
+	}
+	return ""
+}
+
+func (x *LoginResp) GetRefreshToken() string {
+	if x != nil {
+		return x.RefreshToken
+	}
+	return ""
+}
+
+func (x *LoginResp) GetExpires() int64 {
+	if x != nil {
+		return x.Expires
+	}
+	return 0
+}
+
+func (x *LoginResp) GetUserId() int64 {
+	if x != nil {
+		return x.UserId
+	}
+	return 0
+}
+
+func (x *LoginResp) GetUsername() string {
+	if x != nil {
+		return x.Username
+	}
+	return ""
+}
+
+func (x *LoginResp) GetNickname() string {
+	if x != nil {
+		return x.Nickname
+	}
+	return ""
+}
+
+func (x *LoginResp) GetMemberType() string {
+	if x != nil {
+		return x.MemberType
+	}
+	return ""
+}
+
+func (x *LoginResp) GetPerms() []string {
+	if x != nil {
+		return x.Perms
+	}
+	return nil
+}
+
+type RefreshTokenReq struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	RefreshToken  string                 `protobuf:"bytes,1,opt,name=refreshToken,proto3" json:"refreshToken,omitempty"`
+	ProductCode   string                 `protobuf:"bytes,2,opt,name=productCode,proto3" json:"productCode,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *RefreshTokenReq) Reset() {
+	*x = RefreshTokenReq{}
+	mi := &file_pb_perm_proto_msgTypes[5]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *RefreshTokenReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RefreshTokenReq) ProtoMessage() {}
+
+func (x *RefreshTokenReq) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[5]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RefreshTokenReq.ProtoReflect.Descriptor instead.
+func (*RefreshTokenReq) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *RefreshTokenReq) GetRefreshToken() string {
+	if x != nil {
+		return x.RefreshToken
+	}
+	return ""
+}
+
+func (x *RefreshTokenReq) GetProductCode() string {
+	if x != nil {
+		return x.ProductCode
+	}
+	return ""
+}
+
+type RefreshTokenResp struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	AccessToken   string                 `protobuf:"bytes,1,opt,name=accessToken,proto3" json:"accessToken,omitempty"`
+	RefreshToken  string                 `protobuf:"bytes,2,opt,name=refreshToken,proto3" json:"refreshToken,omitempty"`
+	Expires       int64                  `protobuf:"varint,3,opt,name=expires,proto3" json:"expires,omitempty"`
+	Perms         []string               `protobuf:"bytes,4,rep,name=perms,proto3" json:"perms,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *RefreshTokenResp) Reset() {
+	*x = RefreshTokenResp{}
+	mi := &file_pb_perm_proto_msgTypes[6]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *RefreshTokenResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RefreshTokenResp) ProtoMessage() {}
+
+func (x *RefreshTokenResp) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[6]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RefreshTokenResp.ProtoReflect.Descriptor instead.
+func (*RefreshTokenResp) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *RefreshTokenResp) GetAccessToken() string {
+	if x != nil {
+		return x.AccessToken
+	}
+	return ""
+}
+
+func (x *RefreshTokenResp) GetRefreshToken() string {
+	if x != nil {
+		return x.RefreshToken
+	}
+	return ""
+}
+
+func (x *RefreshTokenResp) GetExpires() int64 {
+	if x != nil {
+		return x.Expires
+	}
+	return 0
+}
+
+func (x *RefreshTokenResp) GetPerms() []string {
+	if x != nil {
+		return x.Perms
+	}
+	return nil
+}
+
+type VerifyTokenReq struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	AccessToken   string                 `protobuf:"bytes,1,opt,name=accessToken,proto3" json:"accessToken,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *VerifyTokenReq) Reset() {
+	*x = VerifyTokenReq{}
+	mi := &file_pb_perm_proto_msgTypes[7]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *VerifyTokenReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*VerifyTokenReq) ProtoMessage() {}
+
+func (x *VerifyTokenReq) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[7]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use VerifyTokenReq.ProtoReflect.Descriptor instead.
+func (*VerifyTokenReq) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *VerifyTokenReq) GetAccessToken() string {
+	if x != nil {
+		return x.AccessToken
+	}
+	return ""
+}
+
+type VerifyTokenResp struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Valid         bool                   `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"`
+	UserId        int64                  `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
+	Username      string                 `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
+	ProductCode   string                 `protobuf:"bytes,4,opt,name=productCode,proto3" json:"productCode,omitempty"`
+	MemberType    string                 `protobuf:"bytes,5,opt,name=memberType,proto3" json:"memberType,omitempty"`
+	Perms         []string               `protobuf:"bytes,6,rep,name=perms,proto3" json:"perms,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *VerifyTokenResp) Reset() {
+	*x = VerifyTokenResp{}
+	mi := &file_pb_perm_proto_msgTypes[8]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *VerifyTokenResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*VerifyTokenResp) ProtoMessage() {}
+
+func (x *VerifyTokenResp) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[8]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use VerifyTokenResp.ProtoReflect.Descriptor instead.
+func (*VerifyTokenResp) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *VerifyTokenResp) GetValid() bool {
+	if x != nil {
+		return x.Valid
+	}
+	return false
+}
+
+func (x *VerifyTokenResp) GetUserId() int64 {
+	if x != nil {
+		return x.UserId
+	}
+	return 0
+}
+
+func (x *VerifyTokenResp) GetUsername() string {
+	if x != nil {
+		return x.Username
+	}
+	return ""
+}
+
+func (x *VerifyTokenResp) GetProductCode() string {
+	if x != nil {
+		return x.ProductCode
+	}
+	return ""
+}
+
+func (x *VerifyTokenResp) GetMemberType() string {
+	if x != nil {
+		return x.MemberType
+	}
+	return ""
+}
+
+func (x *VerifyTokenResp) GetPerms() []string {
+	if x != nil {
+		return x.Perms
+	}
+	return nil
+}
+
+type GetUserPermsReq struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	UserId        int64                  `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"`
+	ProductCode   string                 `protobuf:"bytes,2,opt,name=productCode,proto3" json:"productCode,omitempty"`
+	AppKey        string                 `protobuf:"bytes,3,opt,name=appKey,proto3" json:"appKey,omitempty"`
+	AppSecret     string                 `protobuf:"bytes,4,opt,name=appSecret,proto3" json:"appSecret,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetUserPermsReq) Reset() {
+	*x = GetUserPermsReq{}
+	mi := &file_pb_perm_proto_msgTypes[9]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetUserPermsReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetUserPermsReq) ProtoMessage() {}
+
+func (x *GetUserPermsReq) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[9]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetUserPermsReq.ProtoReflect.Descriptor instead.
+func (*GetUserPermsReq) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *GetUserPermsReq) GetUserId() int64 {
+	if x != nil {
+		return x.UserId
+	}
+	return 0
+}
+
+func (x *GetUserPermsReq) GetProductCode() string {
+	if x != nil {
+		return x.ProductCode
+	}
+	return ""
+}
+
+func (x *GetUserPermsReq) GetAppKey() string {
+	if x != nil {
+		return x.AppKey
+	}
+	return ""
+}
+
+func (x *GetUserPermsReq) GetAppSecret() string {
+	if x != nil {
+		return x.AppSecret
+	}
+	return ""
+}
+
+type GetUserPermsResp struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Perms         []string               `protobuf:"bytes,1,rep,name=perms,proto3" json:"perms,omitempty"`
+	MemberType    string                 `protobuf:"bytes,2,opt,name=memberType,proto3" json:"memberType,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetUserPermsResp) Reset() {
+	*x = GetUserPermsResp{}
+	mi := &file_pb_perm_proto_msgTypes[10]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetUserPermsResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetUserPermsResp) ProtoMessage() {}
+
+func (x *GetUserPermsResp) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[10]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetUserPermsResp.ProtoReflect.Descriptor instead.
+func (*GetUserPermsResp) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *GetUserPermsResp) GetPerms() []string {
+	if x != nil {
+		return x.Perms
+	}
+	return nil
+}
+
+func (x *GetUserPermsResp) GetMemberType() string {
+	if x != nil {
+		return x.MemberType
+	}
+	return ""
+}
+
+var File_pb_perm_proto protoreflect.FileDescriptor
+
+const file_pb_perm_proto_rawDesc = "" +
+	"\n" +
+	"\rpb/perm.proto\x12\x02pb\"J\n" +
+	"\bPermItem\x12\x12\n" +
+	"\x04code\x18\x01 \x01(\tR\x04code\x12\x12\n" +
+	"\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" +
+	"\x06remark\x18\x03 \x01(\tR\x06remark\"n\n" +
+	"\x12SyncPermissionsReq\x12\x16\n" +
+	"\x06appKey\x18\x01 \x01(\tR\x06appKey\x12\x1c\n" +
+	"\tappSecret\x18\x02 \x01(\tR\tappSecret\x12\"\n" +
+	"\x05perms\x18\x03 \x03(\v2\f.pb.PermItemR\x05perms\"a\n" +
+	"\x13SyncPermissionsResp\x12\x14\n" +
+	"\x05added\x18\x01 \x01(\x03R\x05added\x12\x18\n" +
+	"\aupdated\x18\x02 \x01(\x03R\aupdated\x12\x1a\n" +
+	"\bdisabled\x18\x03 \x01(\x03R\bdisabled\"d\n" +
+	"\bLoginReq\x12 \n" +
+	"\vproductCode\x18\x01 \x01(\tR\vproductCode\x12\x1a\n" +
+	"\busername\x18\x02 \x01(\tR\busername\x12\x1a\n" +
+	"\bpassword\x18\x03 \x01(\tR\bpassword\"\xf1\x01\n" +
+	"\tLoginResp\x12 \n" +
+	"\vaccessToken\x18\x01 \x01(\tR\vaccessToken\x12\"\n" +
+	"\frefreshToken\x18\x02 \x01(\tR\frefreshToken\x12\x18\n" +
+	"\aexpires\x18\x03 \x01(\x03R\aexpires\x12\x16\n" +
+	"\x06userId\x18\x04 \x01(\x03R\x06userId\x12\x1a\n" +
+	"\busername\x18\x05 \x01(\tR\busername\x12\x1a\n" +
+	"\bnickname\x18\x06 \x01(\tR\bnickname\x12\x1e\n" +
+	"\n" +
+	"memberType\x18\a \x01(\tR\n" +
+	"memberType\x12\x14\n" +
+	"\x05perms\x18\b \x03(\tR\x05perms\"W\n" +
+	"\x0fRefreshTokenReq\x12\"\n" +
+	"\frefreshToken\x18\x01 \x01(\tR\frefreshToken\x12 \n" +
+	"\vproductCode\x18\x02 \x01(\tR\vproductCode\"\x88\x01\n" +
+	"\x10RefreshTokenResp\x12 \n" +
+	"\vaccessToken\x18\x01 \x01(\tR\vaccessToken\x12\"\n" +
+	"\frefreshToken\x18\x02 \x01(\tR\frefreshToken\x12\x18\n" +
+	"\aexpires\x18\x03 \x01(\x03R\aexpires\x12\x14\n" +
+	"\x05perms\x18\x04 \x03(\tR\x05perms\"2\n" +
+	"\x0eVerifyTokenReq\x12 \n" +
+	"\vaccessToken\x18\x01 \x01(\tR\vaccessToken\"\xb3\x01\n" +
+	"\x0fVerifyTokenResp\x12\x14\n" +
+	"\x05valid\x18\x01 \x01(\bR\x05valid\x12\x16\n" +
+	"\x06userId\x18\x02 \x01(\x03R\x06userId\x12\x1a\n" +
+	"\busername\x18\x03 \x01(\tR\busername\x12 \n" +
+	"\vproductCode\x18\x04 \x01(\tR\vproductCode\x12\x1e\n" +
+	"\n" +
+	"memberType\x18\x05 \x01(\tR\n" +
+	"memberType\x12\x14\n" +
+	"\x05perms\x18\x06 \x03(\tR\x05perms\"\x81\x01\n" +
+	"\x0fGetUserPermsReq\x12\x16\n" +
+	"\x06userId\x18\x01 \x01(\x03R\x06userId\x12 \n" +
+	"\vproductCode\x18\x02 \x01(\tR\vproductCode\x12\x16\n" +
+	"\x06appKey\x18\x03 \x01(\tR\x06appKey\x12\x1c\n" +
+	"\tappSecret\x18\x04 \x01(\tR\tappSecret\"H\n" +
+	"\x10GetUserPermsResp\x12\x14\n" +
+	"\x05perms\x18\x01 \x03(\tR\x05perms\x12\x1e\n" +
+	"\n" +
+	"memberType\x18\x02 \x01(\tR\n" +
+	"memberType2\xa5\x02\n" +
+	"\vPermService\x12B\n" +
+	"\x0fSyncPermissions\x12\x16.pb.SyncPermissionsReq\x1a\x17.pb.SyncPermissionsResp\x12$\n" +
+	"\x05Login\x12\f.pb.LoginReq\x1a\r.pb.LoginResp\x129\n" +
+	"\fRefreshToken\x12\x13.pb.RefreshTokenReq\x1a\x14.pb.RefreshTokenResp\x126\n" +
+	"\vVerifyToken\x12\x12.pb.VerifyTokenReq\x1a\x13.pb.VerifyTokenResp\x129\n" +
+	"\fGetUserPerms\x12\x13.pb.GetUserPermsReq\x1a\x14.pb.GetUserPermsRespB\x1eZ\x1cgithub.com/xiaozi/permlib/pbb\x06proto3"
+
+var (
+	file_pb_perm_proto_rawDescOnce sync.Once
+	file_pb_perm_proto_rawDescData []byte
+)
+
+func file_pb_perm_proto_rawDescGZIP() []byte {
+	file_pb_perm_proto_rawDescOnce.Do(func() {
+		file_pb_perm_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_pb_perm_proto_rawDesc), len(file_pb_perm_proto_rawDesc)))
+	})
+	return file_pb_perm_proto_rawDescData
+}
+
+var file_pb_perm_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+var file_pb_perm_proto_goTypes = []any{
+	(*PermItem)(nil),            // 0: pb.PermItem
+	(*SyncPermissionsReq)(nil),  // 1: pb.SyncPermissionsReq
+	(*SyncPermissionsResp)(nil), // 2: pb.SyncPermissionsResp
+	(*LoginReq)(nil),            // 3: pb.LoginReq
+	(*LoginResp)(nil),           // 4: pb.LoginResp
+	(*RefreshTokenReq)(nil),     // 5: pb.RefreshTokenReq
+	(*RefreshTokenResp)(nil),    // 6: pb.RefreshTokenResp
+	(*VerifyTokenReq)(nil),      // 7: pb.VerifyTokenReq
+	(*VerifyTokenResp)(nil),     // 8: pb.VerifyTokenResp
+	(*GetUserPermsReq)(nil),     // 9: pb.GetUserPermsReq
+	(*GetUserPermsResp)(nil),    // 10: pb.GetUserPermsResp
+}
+var file_pb_perm_proto_depIdxs = []int32{
+	0,  // 0: pb.SyncPermissionsReq.perms:type_name -> pb.PermItem
+	1,  // 1: pb.PermService.SyncPermissions:input_type -> pb.SyncPermissionsReq
+	3,  // 2: pb.PermService.Login:input_type -> pb.LoginReq
+	5,  // 3: pb.PermService.RefreshToken:input_type -> pb.RefreshTokenReq
+	7,  // 4: pb.PermService.VerifyToken:input_type -> pb.VerifyTokenReq
+	9,  // 5: pb.PermService.GetUserPerms:input_type -> pb.GetUserPermsReq
+	2,  // 6: pb.PermService.SyncPermissions:output_type -> pb.SyncPermissionsResp
+	4,  // 7: pb.PermService.Login:output_type -> pb.LoginResp
+	6,  // 8: pb.PermService.RefreshToken:output_type -> pb.RefreshTokenResp
+	8,  // 9: pb.PermService.VerifyToken:output_type -> pb.VerifyTokenResp
+	10, // 10: pb.PermService.GetUserPerms:output_type -> pb.GetUserPermsResp
+	6,  // [6:11] is the sub-list for method output_type
+	1,  // [1:6] is the sub-list for method input_type
+	1,  // [1:1] is the sub-list for extension type_name
+	1,  // [1:1] is the sub-list for extension extendee
+	0,  // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_pb_perm_proto_init() }
+func file_pb_perm_proto_init() {
+	if File_pb_perm_proto != nil {
+		return
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_pb_perm_proto_rawDesc), len(file_pb_perm_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   11,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_pb_perm_proto_goTypes,
+		DependencyIndexes: file_pb_perm_proto_depIdxs,
+		MessageInfos:      file_pb_perm_proto_msgTypes,
+	}.Build()
+	File_pb_perm_proto = out.File
+	file_pb_perm_proto_goTypes = nil
+	file_pb_perm_proto_depIdxs = nil
+}

+ 85 - 0
pb/perm.proto

@@ -0,0 +1,85 @@
+syntax = "proto3";
+
+package pb;
+
+option go_package = "github.com/xiaozi/permlib/pb";
+
+service PermService {
+  rpc SyncPermissions(SyncPermissionsReq) returns (SyncPermissionsResp);
+  rpc Login(LoginReq) returns (LoginResp);
+  rpc RefreshToken(RefreshTokenReq) returns (RefreshTokenResp);
+  rpc VerifyToken(VerifyTokenReq) returns (VerifyTokenResp);
+  rpc GetUserPerms(GetUserPermsReq) returns (GetUserPermsResp);
+}
+
+message PermItem {
+  string code = 1;
+  string name = 2;
+  string remark = 3;
+}
+
+message SyncPermissionsReq {
+  string appKey = 1;
+  string appSecret = 2;
+  repeated PermItem perms = 3;
+}
+
+message SyncPermissionsResp {
+  int64 added = 1;
+  int64 updated = 2;
+  int64 disabled = 3;
+}
+
+message LoginReq {
+  string productCode = 1;
+  string username = 2;
+  string password = 3;
+}
+
+message LoginResp {
+  string accessToken = 1;
+  string refreshToken = 2;
+  int64 expires = 3;
+  int64 userId = 4;
+  string username = 5;
+  string nickname = 6;
+  string memberType = 7;
+  repeated string perms = 8;
+}
+
+message RefreshTokenReq {
+  string refreshToken = 1;
+  string productCode = 2;
+}
+
+message RefreshTokenResp {
+  string accessToken = 1;
+  string refreshToken = 2;
+  int64 expires = 3;
+  repeated string perms = 4;
+}
+
+message VerifyTokenReq {
+  string accessToken = 1;
+}
+
+message VerifyTokenResp {
+  bool valid = 1;
+  int64 userId = 2;
+  string username = 3;
+  string productCode = 4;
+  string memberType = 5;
+  repeated string perms = 6;
+}
+
+message GetUserPermsReq {
+  int64 userId = 1;
+  string productCode = 2;
+  string appKey = 3;
+  string appSecret = 4;
+}
+
+message GetUserPermsResp {
+  repeated string perms = 1;
+  string memberType = 2;
+}

+ 273 - 0
pb/perm_grpc.pb.go

@@ -0,0 +1,273 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.6.1
+// - protoc             v7.34.1
+// source: pb/perm.proto
+
+package pb
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+	PermService_SyncPermissions_FullMethodName = "/pb.PermService/SyncPermissions"
+	PermService_Login_FullMethodName           = "/pb.PermService/Login"
+	PermService_RefreshToken_FullMethodName    = "/pb.PermService/RefreshToken"
+	PermService_VerifyToken_FullMethodName     = "/pb.PermService/VerifyToken"
+	PermService_GetUserPerms_FullMethodName    = "/pb.PermService/GetUserPerms"
+)
+
+// PermServiceClient is the client API for PermService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type PermServiceClient interface {
+	SyncPermissions(ctx context.Context, in *SyncPermissionsReq, opts ...grpc.CallOption) (*SyncPermissionsResp, error)
+	Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error)
+	RefreshToken(ctx context.Context, in *RefreshTokenReq, opts ...grpc.CallOption) (*RefreshTokenResp, error)
+	VerifyToken(ctx context.Context, in *VerifyTokenReq, opts ...grpc.CallOption) (*VerifyTokenResp, error)
+	GetUserPerms(ctx context.Context, in *GetUserPermsReq, opts ...grpc.CallOption) (*GetUserPermsResp, error)
+}
+
+type permServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewPermServiceClient(cc grpc.ClientConnInterface) PermServiceClient {
+	return &permServiceClient{cc}
+}
+
+func (c *permServiceClient) SyncPermissions(ctx context.Context, in *SyncPermissionsReq, opts ...grpc.CallOption) (*SyncPermissionsResp, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(SyncPermissionsResp)
+	err := c.cc.Invoke(ctx, PermService_SyncPermissions_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *permServiceClient) Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(LoginResp)
+	err := c.cc.Invoke(ctx, PermService_Login_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *permServiceClient) RefreshToken(ctx context.Context, in *RefreshTokenReq, opts ...grpc.CallOption) (*RefreshTokenResp, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(RefreshTokenResp)
+	err := c.cc.Invoke(ctx, PermService_RefreshToken_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *permServiceClient) VerifyToken(ctx context.Context, in *VerifyTokenReq, opts ...grpc.CallOption) (*VerifyTokenResp, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(VerifyTokenResp)
+	err := c.cc.Invoke(ctx, PermService_VerifyToken_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *permServiceClient) GetUserPerms(ctx context.Context, in *GetUserPermsReq, opts ...grpc.CallOption) (*GetUserPermsResp, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(GetUserPermsResp)
+	err := c.cc.Invoke(ctx, PermService_GetUserPerms_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// PermServiceServer is the server API for PermService service.
+// All implementations must embed UnimplementedPermServiceServer
+// for forward compatibility.
+type PermServiceServer interface {
+	SyncPermissions(context.Context, *SyncPermissionsReq) (*SyncPermissionsResp, error)
+	Login(context.Context, *LoginReq) (*LoginResp, error)
+	RefreshToken(context.Context, *RefreshTokenReq) (*RefreshTokenResp, error)
+	VerifyToken(context.Context, *VerifyTokenReq) (*VerifyTokenResp, error)
+	GetUserPerms(context.Context, *GetUserPermsReq) (*GetUserPermsResp, error)
+	mustEmbedUnimplementedPermServiceServer()
+}
+
+// UnimplementedPermServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedPermServiceServer struct{}
+
+func (UnimplementedPermServiceServer) SyncPermissions(context.Context, *SyncPermissionsReq) (*SyncPermissionsResp, error) {
+	return nil, status.Error(codes.Unimplemented, "method SyncPermissions not implemented")
+}
+func (UnimplementedPermServiceServer) Login(context.Context, *LoginReq) (*LoginResp, error) {
+	return nil, status.Error(codes.Unimplemented, "method Login not implemented")
+}
+func (UnimplementedPermServiceServer) RefreshToken(context.Context, *RefreshTokenReq) (*RefreshTokenResp, error) {
+	return nil, status.Error(codes.Unimplemented, "method RefreshToken not implemented")
+}
+func (UnimplementedPermServiceServer) VerifyToken(context.Context, *VerifyTokenReq) (*VerifyTokenResp, error) {
+	return nil, status.Error(codes.Unimplemented, "method VerifyToken not implemented")
+}
+func (UnimplementedPermServiceServer) GetUserPerms(context.Context, *GetUserPermsReq) (*GetUserPermsResp, error) {
+	return nil, status.Error(codes.Unimplemented, "method GetUserPerms not implemented")
+}
+func (UnimplementedPermServiceServer) mustEmbedUnimplementedPermServiceServer() {}
+func (UnimplementedPermServiceServer) testEmbeddedByValue()                     {}
+
+// UnsafePermServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to PermServiceServer will
+// result in compilation errors.
+type UnsafePermServiceServer interface {
+	mustEmbedUnimplementedPermServiceServer()
+}
+
+func RegisterPermServiceServer(s grpc.ServiceRegistrar, srv PermServiceServer) {
+	// If the following call panics, it indicates UnimplementedPermServiceServer was
+	// embedded by pointer and is nil.  This will cause panics if an
+	// unimplemented method is ever invoked, so we test this at initialization
+	// time to prevent it from happening at runtime later due to I/O.
+	if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+		t.testEmbeddedByValue()
+	}
+	s.RegisterService(&PermService_ServiceDesc, srv)
+}
+
+func _PermService_SyncPermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SyncPermissionsReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PermServiceServer).SyncPermissions(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PermService_SyncPermissions_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PermServiceServer).SyncPermissions(ctx, req.(*SyncPermissionsReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _PermService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(LoginReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PermServiceServer).Login(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PermService_Login_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PermServiceServer).Login(ctx, req.(*LoginReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _PermService_RefreshToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(RefreshTokenReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PermServiceServer).RefreshToken(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PermService_RefreshToken_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PermServiceServer).RefreshToken(ctx, req.(*RefreshTokenReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _PermService_VerifyToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(VerifyTokenReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PermServiceServer).VerifyToken(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PermService_VerifyToken_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PermServiceServer).VerifyToken(ctx, req.(*VerifyTokenReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _PermService_GetUserPerms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetUserPermsReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PermServiceServer).GetUserPerms(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PermService_GetUserPerms_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PermServiceServer).GetUserPerms(ctx, req.(*GetUserPermsReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// PermService_ServiceDesc is the grpc.ServiceDesc for PermService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var PermService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "pb.PermService",
+	HandlerType: (*PermServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "SyncPermissions",
+			Handler:    _PermService_SyncPermissions_Handler,
+		},
+		{
+			MethodName: "Login",
+			Handler:    _PermService_Login_Handler,
+		},
+		{
+			MethodName: "RefreshToken",
+			Handler:    _PermService_RefreshToken_Handler,
+		},
+		{
+			MethodName: "VerifyToken",
+			Handler:    _PermService_VerifyToken_Handler,
+		},
+		{
+			MethodName: "GetUserPerms",
+			Handler:    _PermService_GetUserPerms_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "pb/perm.proto",
+}

+ 159 - 0
permlib.go

@@ -0,0 +1,159 @@
+package permlib
+
+import (
+	"context"
+	"net/http"
+)
+
+// PermDecl 权限声明,用于静态注册(permgen 插件生成)
+type PermDecl struct {
+	Code string
+	Name string
+}
+
+// RoutePermDecl 路由-权限映射,用于静态注册
+type RoutePermDecl struct {
+	Method   string
+	Path     string
+	PermCode string
+	DataCode string // 配对的 data 权限 code
+}
+
+// FieldPermMap 字段权限映射,用于静态注册(替代反射)
+type FieldPermMap struct {
+	Request  map[string]string // json字段名 → permCode
+	Response map[string]string // json字段名 → permCode
+}
+
+type Engine struct {
+	cfg         Config
+	client      *grpcClient
+	cache       *permCache
+	staticPerms []PermDecl
+	routePerms  map[string]RoutePermDecl // "METHOD /path" → RoutePermDecl
+	fieldPerms  map[string]FieldPermMap  // "METHOD /path" → FieldPermMap
+}
+
+type LoginResult struct {
+	AccessToken  string
+	RefreshToken string
+	Expires      int64
+	UserId       int64
+	Username     string
+	Nickname     string
+	MemberType   string
+	Perms        []string
+}
+
+type RefreshTokenResult struct {
+	AccessToken  string
+	RefreshToken string
+	Expires      int64
+	Perms        []string
+}
+
+type UserPermsResult struct {
+	Perms      []string
+	MemberType string
+}
+
+func New(cfg Config) (*Engine, error) {
+	cfg.defaults()
+
+	if err := cfg.validate(); err != nil {
+		return nil, err
+	}
+
+	client, err := newGRPCClient(cfg.PermServerAddr)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Engine{
+		cfg:        cfg,
+		client:     client,
+		cache:      newPermCache(cfg.CacheTTL),
+		routePerms: make(map[string]RoutePermDecl),
+		fieldPerms: make(map[string]FieldPermMap),
+	}, nil
+}
+
+// RegisterPerms 静态注册权限声明(由 permgen 插件生成的代码调用)
+func (e *Engine) RegisterPerms(perms []PermDecl) {
+	e.staticPerms = append(e.staticPerms, perms...)
+}
+
+// RegisterRoutePerms 静态注册路由-权限映射(由 permgen 插件生成的代码调用)
+func (e *Engine) RegisterRoutePerms(decls []RoutePermDecl) {
+	for _, d := range decls {
+		key := d.Method + " " + d.Path
+		e.routePerms[key] = d
+	}
+}
+
+// RegisterFieldPerms 静态注册字段权限映射(由 permgen 插件生成的代码调用,替代反射)
+func (e *Engine) RegisterFieldPerms(m map[string]FieldPermMap) {
+	for k, v := range m {
+		e.fieldPerms[k] = v
+	}
+}
+
+func (e *Engine) Start(ctx context.Context) error {
+	perms := e.collectPerms()
+	if len(perms) > 0 {
+		if err := e.syncPerms(ctx, perms); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Login 调用权限系统 gRPC Login 接口
+func (e *Engine) Login(ctx context.Context, username, password string) (*LoginResult, error) {
+	resp, err := e.client.login(ctx, e.cfg.ProductCode, username, password)
+	if err != nil {
+		return nil, err
+	}
+	return &LoginResult{
+		AccessToken:  resp.AccessToken,
+		RefreshToken: resp.RefreshToken,
+		Expires:      resp.Expires,
+		UserId:       resp.UserId,
+		Username:     resp.Username,
+		Nickname:     resp.Nickname,
+		MemberType:   resp.MemberType,
+		Perms:        resp.Perms,
+	}, nil
+}
+
+// RefreshToken 调用权限系统 gRPC RefreshToken 接口
+func (e *Engine) RefreshToken(ctx context.Context, refreshToken string) (*RefreshTokenResult, error) {
+	resp, err := e.client.refreshToken(ctx, refreshToken, e.cfg.ProductCode)
+	if err != nil {
+		return nil, err
+	}
+	return &RefreshTokenResult{
+		AccessToken:  resp.AccessToken,
+		RefreshToken: resp.RefreshToken,
+		Expires:      resp.Expires,
+		Perms:        resp.Perms,
+	}, nil
+}
+
+// GetUserPerms 查询指定用户在当前产品下的权限列表,使用 appKey/appSecret 做服务间认证
+// 适用于定时任务、服务间调用等不走 token 验证的场景
+func (e *Engine) GetUserPerms(ctx context.Context, userId int64) (*UserPermsResult, error) {
+	resp, err := e.client.getUserPerms(ctx, e.cfg.AppKey, e.cfg.AppSecret, userId, e.cfg.ProductCode)
+	if err != nil {
+		return nil, err
+	}
+	return &UserPermsResult{
+		Perms:      resp.Perms,
+		MemberType: resp.MemberType,
+	}, nil
+}
+
+// AuthMiddleware 返回鉴权 handler,供产品端中间件直接调用
+func (e *Engine) AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
+	return e.authMiddleware(next)
+}

+ 17 - 0
sync.go

@@ -0,0 +1,17 @@
+package permlib
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/xiaozi/permlib/pb"
+)
+
+func (e *Engine) syncPerms(ctx context.Context, perms []*pb.PermItem) error {
+	resp, err := e.client.syncPermissions(ctx, e.cfg.AppKey, e.cfg.AppSecret, perms)
+	if err != nil {
+		return fmt.Errorf("permlib: sync permissions failed: %w", err)
+	}
+	_ = resp
+	return nil
+}