diff --git a/go.mod b/go.mod index aa04e4b..a4b9a54 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-cinch/common/i18n v1.0.6 github.com/go-cinch/common/id v1.0.4 github.com/go-cinch/common/idempotent v1.0.4 - github.com/go-cinch/common/jwt v1.0.3 + github.com/go-cinch/common/jwt v1.0.4 github.com/go-cinch/common/log v1.1.1 github.com/go-cinch/common/middleware/i18n v1.0.5 github.com/go-cinch/common/middleware/logging v1.0.0 diff --git a/go.sum b/go.sum index 0033cae..33f0133 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/go-cinch/common/id v1.0.4 h1:bMjcxMq5lL19o2a/ty6OnS8tIn7kc98FznAq2vyp github.com/go-cinch/common/id v1.0.4/go.mod h1:ZeuOCZyx1ZKuCexzLRW0gBCnwDUKH8XDs1S0dNv4Z+k= github.com/go-cinch/common/idempotent v1.0.4 h1:E/Ab+FHWjs1ym0U0mips6MyHEzucEdedmVWA22U3du8= github.com/go-cinch/common/idempotent v1.0.4/go.mod h1:t4rBikz+RhpmF0tdGx+rm0Z1FzDy/6GtelgFB7RlaiY= -github.com/go-cinch/common/jwt v1.0.3 h1:/jznvjevW+2KsmWm666O1NxkHLSfVmrjoWwkuWAqewk= -github.com/go-cinch/common/jwt v1.0.3/go.mod h1:++p7kddWh9SyCYIYWEb5mfiR+ljKC3I5puiyjz7oGNs= +github.com/go-cinch/common/jwt v1.0.4 h1:PWLIQ/HaMhQSkyUafBCjQDNXwz1mzeCzsTCbgeKEeRI= +github.com/go-cinch/common/jwt v1.0.4/go.mod h1:++p7kddWh9SyCYIYWEb5mfiR+ljKC3I5puiyjz7oGNs= github.com/go-cinch/common/log v1.1.1 h1:9ot3Qw4BKDfOZ7IC/3ZreuYJb953YD7pmM6ev6xT9FY= github.com/go-cinch/common/log v1.1.1/go.mod h1:O4k/ArEdZS6c+YPKdESWJnVfhQ0QlwtIQ6mYxJZt5po= github.com/go-cinch/common/middleware/i18n v1.0.5 h1:SmxEMljqYF8xraaaa3KZgvNbKIE06ShQAZ34w87wkvs= diff --git a/internal/biz/action.go b/internal/biz/action.go index 7dd8eab..220ddbe 100644 --- a/internal/biz/action.go +++ b/internal/biz/action.go @@ -49,8 +49,8 @@ type ActionRepo interface { Update(ctx context.Context, item *UpdateAction) error Delete(ctx context.Context, ids ...uint64) error CodeExists(ctx context.Context, code string) error - Permission(ctx context.Context, code string, req CheckPermission) bool - MatchResource(ctx context.Context, resource string, req CheckPermission) bool + Permission(ctx context.Context, code string, req *CheckPermission) bool + MatchResource(ctx context.Context, resource string, req *CheckPermission) bool } type ActionUseCase struct { diff --git a/internal/biz/permission.go b/internal/biz/permission.go index f37c606..0f4c42d 100644 --- a/internal/biz/permission.go +++ b/internal/biz/permission.go @@ -20,7 +20,7 @@ type CheckPermission struct { } type PermissionRepo interface { - Check(ctx context.Context, item CheckPermission) bool + Check(ctx context.Context, item *CheckPermission) bool GetByUserCode(ctx context.Context, code string) *Permission } @@ -36,7 +36,7 @@ func NewPermissionUseCase(c *conf.Bootstrap, repo PermissionRepo) *PermissionUse } } -func (uc *PermissionUseCase) Check(ctx context.Context, item CheckPermission) (rp bool) { +func (uc *PermissionUseCase) Check(ctx context.Context, item *CheckPermission) (rp bool) { rp = uc.repo.Check(ctx, item) return } diff --git a/internal/biz/user.go b/internal/biz/user.go index e4e0ea9..3efeaf8 100644 --- a/internal/biz/user.go +++ b/internal/biz/user.go @@ -124,7 +124,7 @@ type UserRepo interface { Update(ctx context.Context, item *UpdateUser) error Delete(ctx context.Context, ids ...uint64) error LastLogin(ctx context.Context, username string) error - WrongPwd(ctx context.Context, req LoginTime) error + WrongPwd(ctx context.Context, req *LoginTime) error UpdatePassword(ctx context.Context, item *User) error IdExists(ctx context.Context, id uint64) error } @@ -211,7 +211,7 @@ func (uc *UserUseCase) find(ctx context.Context, action string, condition *FindU func (uc *UserUseCase) InfoFromCtx(ctx context.Context) (rp *UserInfo) { user := jwt.FromServerContext(ctx) - return uc.Info(ctx, user.Code) + return uc.Info(ctx, user.Attrs["code"]) } func (uc *UserUseCase) Info(ctx context.Context, code string) (rp *UserInfo) { @@ -259,8 +259,10 @@ func (uc *UserUseCase) Login(ctx context.Context, item *Login) (rp *LoginToken, return } authUser := jwt.User{ - Code: status.Code, - Platform: status.Platform, + Attrs: map[string]string{ + "code": status.Code, + "platform": status.Platform, + }, } token, expireTime := authUser.CreateToken(uc.c.Server.Jwt.Key, uc.c.Server.Jwt.Expires) rp.Token = token @@ -279,7 +281,7 @@ func (uc *UserUseCase) LastLogin(ctx context.Context, username string) error { }) } -func (uc *UserUseCase) WrongPwd(ctx context.Context, req LoginTime) error { +func (uc *UserUseCase) WrongPwd(ctx context.Context, req *LoginTime) error { return uc.tx.Tx(ctx, func(ctx context.Context) (err error) { err = uc.repo.WrongPwd(ctx, req) if err != nil { diff --git a/internal/biz/whitelist.go b/internal/biz/whitelist.go index 2d3afe1..ba48894 100644 --- a/internal/biz/whitelist.go +++ b/internal/biz/whitelist.go @@ -12,7 +12,6 @@ import ( const ( WhitelistPermissionCategory uint32 = iota WhitelistJwtCategory - WhitelistIdempotentCategory ) type Whitelist struct { @@ -28,8 +27,8 @@ type FindWhitelist struct { } type HasWhitelist struct { - Category uint32 `json:"category"` - Permission CheckPermission `json:"permission"` + Category uint32 `json:"category"` + Permission *CheckPermission `json:"permission"` } type FindWhitelistCache struct { diff --git a/internal/data/action.go b/internal/data/action.go index e983603..be120ce 100644 --- a/internal/data/action.go +++ b/internal/data/action.go @@ -184,7 +184,7 @@ func (ro actionRepo) WordExists(ctx context.Context, word string) (ok bool) { return } -func (ro actionRepo) Permission(ctx context.Context, code string, req biz.CheckPermission) (pass bool) { +func (ro actionRepo) Permission(ctx context.Context, code string, req *biz.CheckPermission) (pass bool) { arr := strings.Split(code, ",") for _, item := range arr { pass = ro.permission(ctx, item, req) @@ -195,7 +195,7 @@ func (ro actionRepo) Permission(ctx context.Context, code string, req biz.CheckP return } -func (ro actionRepo) permission(ctx context.Context, code string, req biz.CheckPermission) (pass bool) { +func (ro actionRepo) permission(ctx context.Context, code string, req *biz.CheckPermission) (pass bool) { if code == "" { return } @@ -203,7 +203,7 @@ func (ro actionRepo) permission(ctx context.Context, code string, req biz.CheckP return ro.MatchResource(ctx, action.Resource, req) } -func (actionRepo) MatchResource(_ context.Context, resource string, req biz.CheckPermission) (pass bool) { +func (actionRepo) MatchResource(_ context.Context, resource string, req *biz.CheckPermission) (pass bool) { if resource == "" { // empty resource no need match return diff --git a/internal/data/permission.go b/internal/data/permission.go index 43e662a..acaf6dc 100644 --- a/internal/data/permission.go +++ b/internal/data/permission.go @@ -23,7 +23,7 @@ func NewPermissionRepo(data *Data, action biz.ActionRepo, hotspot biz.HotspotRep } } -func (ro permissionRepo) Check(ctx context.Context, item biz.CheckPermission) (pass bool) { +func (ro permissionRepo) Check(ctx context.Context, item *biz.CheckPermission) (pass bool) { user := ro.hotspot.GetUserByCode(ctx, item.UserCode) // 1. check default permission defaultAction := ro.hotspot.GetActionByWord(ctx, "default") diff --git a/internal/data/user.go b/internal/data/user.go index 05c82c4..3e7b798 100644 --- a/internal/data/user.go +++ b/internal/data/user.go @@ -234,7 +234,7 @@ func (ro userRepo) LastLogin(ctx context.Context, username string) (err error) { return } -func (ro userRepo) WrongPwd(ctx context.Context, req biz.LoginTime) (err error) { +func (ro userRepo) WrongPwd(ctx context.Context, req *biz.LoginTime) (err error) { oldItem, err := ro.GetByUsername(ctx, req.Username) if err != nil { return diff --git a/internal/pkg/task/task.go b/internal/pkg/task/task.go index f138ecf..fc7824b 100644 --- a/internal/pkg/task/task.go +++ b/internal/pkg/task/task.go @@ -83,7 +83,7 @@ func process(t task) (err error) { case t.c.Task.Group.LoginFailed: var req biz.LoginTime utils.Json2Struct(&req, t.payload.Payload) - err = t.user.WrongPwd(ctx, req) + err = t.user.WrongPwd(ctx, &req) case t.c.Task.Group.LoginLast: var req biz.LoginTime utils.Json2Struct(&req, t.payload.Payload) diff --git a/internal/server/middleware/permission.go b/internal/server/middleware/permission.go new file mode 100644 index 0000000..dca31ad --- /dev/null +++ b/internal/server/middleware/permission.go @@ -0,0 +1,178 @@ +package middleware + +import ( + "context" + "errors" + "net/http" + "strings" + "time" + + "auth/api/auth" + "auth/internal/biz" + "auth/internal/conf" + "github.com/go-cinch/common/copierx" + jwtLocal "github.com/go-cinch/common/jwt" + "github.com/go-cinch/common/log" + "github.com/go-cinch/common/utils" + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/transport" + kratosHttp "github.com/go-kratos/kratos/v2/transport/http" + jwtV4 "github.com/golang-jwt/jwt/v4" + "github.com/redis/go-redis/v9" + "google.golang.org/protobuf/types/known/emptypb" +) + +const ( + pubURIPrefix = "/pub/" + jwtTokenCachePrefix = "jwt.token" + jwtTokenCacheExpire = 10 * time.Minute + permissionHeaderMethod = "x-original-method" + permissionHeaderURI = "x-permission-uri" +) + +func Permission(c *conf.Bootstrap, client redis.UniversalClient, whitelist *biz.WhitelistUseCase) func(handler middleware.Handler) middleware.Handler { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (rp interface{}, err error) { + tr, _ := transport.FromServerContext(ctx) + if tr.Kind() == transport.KindGRPC { + // 1. grpc api for internal no need check + // tip: u can add ur logic if need check grpc + return handler(ctx, req) + } + // check http api + var uri string + operation := tr.Operation() + if ht, ok := tr.(kratosHttp.Transporter); ok { + uri = ht.Request().URL.Path + } + // 2. public api no need check + if strings.Contains(uri, pubURIPrefix) { + return handler(ctx, req) + } + if operation == auth.OperationAuthPermission && permissionWhitelist(ctx, whitelist, req) { + // for nginx auth_request + // 3. permission whitelist api no need check + rp = &emptypb.Empty{} + return + } else if jwtWhitelist(ctx, whitelist) { + // 4. jwt whitelist api no need check + return handler(ctx, req) + } + user, err := parseJwt(ctx, c, client, c.Server.Jwt.Key) + if err != nil { + return + } + // pass user info into ctx + ctx = jwtLocal.NewServerContextByUser(ctx, *user) + return handler(ctx, req) + } + } +} + +func permissionWhitelist(ctx context.Context, whitelist *biz.WhitelistUseCase, req interface{}) (ok bool) { + tr, _ := transport.FromServerContext(ctx) + var r biz.CheckPermission + copierx.Copy(&r, req) + // get from header if exist + method := tr.RequestHeader().Get(permissionHeaderMethod) + if method != "" { + r.Method = method + } + uri := tr.RequestHeader().Get(permissionHeaderURI) + if uri != "" { + r.URI = uri + } + // public api no need check + if strings.Contains(r.URI, pubURIPrefix) { + return true + } + log. + WithContext(ctx). + Info("method: %s, uri: %s, resource: %s", r.Method, r.URI, r.Resource) + // skip options + if r.Method == http.MethodOptions || r.Method == http.MethodHead { + return + } + // check if it is on the whitelist + ok = whitelist.Has(ctx, &biz.HasWhitelist{ + Category: biz.WhitelistPermissionCategory, + Permission: &r, + }) + // override params + v, ok2 := req.(*auth.PermissionRequest) + if ok2 { + v.Method = &r.Method + v.Uri = &r.URI + req = v + return + } + return +} + +func jwtWhitelist(ctx context.Context, whitelist *biz.WhitelistUseCase) bool { + tr, _ := transport.FromServerContext(ctx) + return whitelist.Has(ctx, &biz.HasWhitelist{ + Category: biz.WhitelistJwtCategory, + Permission: &biz.CheckPermission{ + Resource: tr.Operation(), + }, + }) +} + +func parseJwt(ctx context.Context, c *conf.Bootstrap, client redis.UniversalClient, jwtKey string) (user *jwtLocal.User, err error) { + user = jwtLocal.FromServerContext(ctx) + if user.Token == "" { + err = biz.ErrJwtMissingToken(ctx) + return + } + key := strings.Join([]string{c.Name, jwtTokenCachePrefix, utils.StructMd5(user.Token)}, ".") + res, _ := client.Get(ctx, key).Result() + if res != "" { + utils.Json2Struct(user, res) + return + } + + // parse Authorization jwt token to get user info + var info *jwtV4.Token + info, err = parseToken(ctx, jwtKey, user.Token) + if err != nil { + return + } + ctx = jwtLocal.NewServerContext(ctx, info.Claims, "code", "platform") + user = jwtLocal.FromServerContext(ctx) + client.Set(ctx, key, utils.Struct2Json(user), jwtTokenCacheExpire) + return +} + +func parseToken(ctx context.Context, key, jwtToken string) (info *jwtV4.Token, err error) { + info, err = jwtV4.Parse(jwtToken, func(token *jwtV4.Token) (rp interface{}, err error) { + rp = []byte(key) + return + }) + if err != nil { + var ve *jwtV4.ValidationError + ok := errors.As(err, &ve) + if !ok { + return + } + if ve.Errors&jwtV4.ValidationErrorMalformed != 0 { + err = biz.ErrJwtTokenInvalid(ctx) + return + } + if ve.Errors&(jwtV4.ValidationErrorExpired|jwtV4.ValidationErrorNotValidYet) != 0 { + err = biz.ErrJwtTokenExpired(ctx) + return + } + err = biz.ErrJwtTokenParseFail(ctx) + return + } + if !info.Valid { + err = biz.ErrJwtTokenParseFail(ctx) + return + } + if info.Method != jwtV4.SigningMethodHS512 { + err = biz.ErrJwtUnSupportSigningMethod(ctx) + return + } + return +} diff --git a/internal/service/auth.go b/internal/service/auth.go index 71c66af..6b2a872 100644 --- a/internal/service/auth.go +++ b/internal/service/auth.go @@ -139,8 +139,8 @@ func (s *AuthService) Permission(ctx context.Context, req *auth.PermissionReques defer span.End() rp = &emptypb.Empty{} user := jwt.FromServerContext(ctx) - r := biz.CheckPermission{ - UserCode: user.Code, + r := &biz.CheckPermission{ + UserCode: user.Attrs["code"], } if req.Resource != nil { r.Resource = *req.Resource @@ -156,10 +156,12 @@ func (s *AuthService) Permission(ctx context.Context, req *auth.PermissionReques err = biz.ErrNoPermission(ctx) return } - info := s.user.Info(ctx, user.Code) + info := s.user.Info(ctx, user.Attrs["code"]) jwt.AppendToReplyHeader(ctx, jwt.User{ - Code: info.Code, - Platform: info.Platform, + Attrs: map[string]string{ + "code": info.Code, + "platform": info.Platform, + }, }) return } @@ -171,8 +173,8 @@ func (s *AuthService) Info(ctx context.Context, _ *emptypb.Empty) (rp *auth.Info rp = &auth.InfoReply{} rp.Permission = &auth.Permission{} user := jwt.FromServerContext(ctx) - res := s.user.Info(ctx, user.Code) - permission := s.permission.GetByUserCode(ctx, user.Code) + res := s.user.Info(ctx, user.Attrs["code"]) + permission := s.permission.GetByUserCode(ctx, user.Attrs["code"]) copierx.Copy(&rp.Permission, permission) copierx.Copy(&rp, res) return