diff --git a/.gitignore b/.gitignore index d7d485f..59ba36c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ go.sum account/ gen_result/ -etc/permission.yaml \ No newline at end of file +etc/permission.yaml +./client \ No newline at end of file diff --git a/client/permissionservice/permission_service.go b/client/permissionservice/permission_service.go new file mode 100644 index 0000000..fdae9cb --- /dev/null +++ b/client/permissionservice/permission_service.go @@ -0,0 +1,45 @@ +// Code generated by goctl. DO NOT EDIT. +// Source: permission.proto + +package permissionservice + +import ( + "context" + + "ark-permission/gen_result/pb/permission" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + AuthorizationReq = permission.AuthorizationReq + CancelOneTimeTokenReq = permission.CancelOneTimeTokenReq + CancelTokenReq = permission.CancelTokenReq + CreateOneTimeTokenReq = permission.CreateOneTimeTokenReq + CreateOneTimeTokenResp = permission.CreateOneTimeTokenResp + DoTokenByDeviceIDReq = permission.DoTokenByDeviceIDReq + DoTokenByUIDReq = permission.DoTokenByUIDReq + OKResp = permission.OKResp + QueryTokenByUIDReq = permission.QueryTokenByUIDReq + RefreshTokenReq = permission.RefreshTokenReq + RefreshTokenResp = permission.RefreshTokenResp + Token = permission.Token + TokenResp = permission.TokenResp + Tokens = permission.Tokens + ValidationTokenReq = permission.ValidationTokenReq + ValidationTokenResp = permission.ValidationTokenResp + + PermissionService interface { + } + + defaultPermissionService struct { + cli zrpc.Client + } +) + +func NewPermissionService(cli zrpc.Client) PermissionService { + return &defaultPermissionService{ + cli: cli, + } +} diff --git a/client/roleservice/role_service.go b/client/roleservice/role_service.go new file mode 100644 index 0000000..8d7e2e2 --- /dev/null +++ b/client/roleservice/role_service.go @@ -0,0 +1,51 @@ +// Code generated by goctl. DO NOT EDIT. +// Source: permission.proto + +package roleservice + +import ( + "context" + + "ark-permission/gen_result/pb/permission" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + AuthorizationReq = permission.AuthorizationReq + CancelOneTimeTokenReq = permission.CancelOneTimeTokenReq + CancelTokenReq = permission.CancelTokenReq + CreateOneTimeTokenReq = permission.CreateOneTimeTokenReq + CreateOneTimeTokenResp = permission.CreateOneTimeTokenResp + DoTokenByDeviceIDReq = permission.DoTokenByDeviceIDReq + DoTokenByUIDReq = permission.DoTokenByUIDReq + OKResp = permission.OKResp + QueryTokenByUIDReq = permission.QueryTokenByUIDReq + RefreshTokenReq = permission.RefreshTokenReq + RefreshTokenResp = permission.RefreshTokenResp + Token = permission.Token + TokenResp = permission.TokenResp + Tokens = permission.Tokens + ValidationTokenReq = permission.ValidationTokenReq + ValidationTokenResp = permission.ValidationTokenResp + + RoleService interface { + Ping(ctx context.Context, in *OKResp, opts ...grpc.CallOption) (*OKResp, error) + } + + defaultRoleService struct { + cli zrpc.Client + } +) + +func NewRoleService(cli zrpc.Client) RoleService { + return &defaultRoleService{ + cli: cli, + } +} + +func (m *defaultRoleService) Ping(ctx context.Context, in *OKResp, opts ...grpc.CallOption) (*OKResp, error) { + client := permission.NewRoleServiceClient(m.cli.Conn()) + return client.Ping(ctx, in, opts...) +} diff --git a/tokenservice/token_service.go b/client/tokenservice/token_service.go similarity index 66% rename from tokenservice/token_service.go rename to client/tokenservice/token_service.go index 074dcad..617dc38 100644 --- a/tokenservice/token_service.go +++ b/client/tokenservice/token_service.go @@ -14,12 +14,14 @@ import ( type ( AuthorizationReq = permission.AuthorizationReq + CancelOneTimeTokenReq = permission.CancelOneTimeTokenReq CancelTokenReq = permission.CancelTokenReq CreateOneTimeTokenReq = permission.CreateOneTimeTokenReq CreateOneTimeTokenResp = permission.CreateOneTimeTokenResp DoTokenByDeviceIDReq = permission.DoTokenByDeviceIDReq DoTokenByUIDReq = permission.DoTokenByUIDReq OKResp = permission.OKResp + QueryTokenByUIDReq = permission.QueryTokenByUIDReq RefreshTokenReq = permission.RefreshTokenReq RefreshTokenResp = permission.RefreshTokenResp Token = permission.Token @@ -35,20 +37,20 @@ type ( RefreshToken(ctx context.Context, in *RefreshTokenReq, opts ...grpc.CallOption) (*RefreshTokenResp, error) // CancelToken 取消 Token,也包含他裡面的 One Time Toke CancelToken(ctx context.Context, in *CancelTokenReq, opts ...grpc.CallOption) (*OKResp, error) - // CancelTokenByUID 取消 Token (取消這個用戶從不同 Device 登入的所有 Token),也包含他裡面的 One Time Toke - CancelTokenByUID(ctx context.Context, in *DoTokenByUIDReq, opts ...grpc.CallOption) (*OKResp, error) - // CancelTokenByDeviceID 取消 Token - CancelTokenByDeviceID(ctx context.Context, in *DoTokenByDeviceIDReq, opts ...grpc.CallOption) (*OKResp, error) // ValidationToken 驗證這個 Token 有沒有效 ValidationToken(ctx context.Context, in *ValidationTokenReq, opts ...grpc.CallOption) (*ValidationTokenResp, error) - // GetUserTokensByDeviceIDs 取得目前所對應的 DeviceID 所存在的 Tokens - GetUserTokensByDeviceID(ctx context.Context, in *DoTokenByDeviceIDReq, opts ...grpc.CallOption) (*Tokens, error) - // GetUserTokensByUID 取得目前所對應的 UID 所存在的 Tokens - GetUserTokensByUID(ctx context.Context, in *DoTokenByUIDReq, opts ...grpc.CallOption) (*Tokens, error) + // CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出 + CancelTokens(ctx context.Context, in *DoTokenByUIDReq, opts ...grpc.CallOption) (*OKResp, error) + // CancelTokenByDeviceId 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token + CancelTokenByDeviceId(ctx context.Context, in *DoTokenByDeviceIDReq, opts ...grpc.CallOption) (*OKResp, error) + // GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens + GetUserTokensByDeviceId(ctx context.Context, in *DoTokenByDeviceIDReq, opts ...grpc.CallOption) (*Tokens, error) + // GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens + GetUserTokensByUid(ctx context.Context, in *QueryTokenByUIDReq, opts ...grpc.CallOption) (*Tokens, error) // NewOneTimeToken 建立一次性使用,例如:RefreshToken NewOneTimeToken(ctx context.Context, in *CreateOneTimeTokenReq, opts ...grpc.CallOption) (*CreateOneTimeTokenResp, error) // CancelOneTimeToken 取消一次性使用 - CancelOneTimeToken(ctx context.Context, in *CreateOneTimeTokenReq, opts ...grpc.CallOption) (*CreateOneTimeTokenResp, error) + CancelOneTimeToken(ctx context.Context, in *CancelOneTimeTokenReq, opts ...grpc.CallOption) (*OKResp, error) } defaultTokenService struct { @@ -80,34 +82,34 @@ func (m *defaultTokenService) CancelToken(ctx context.Context, in *CancelTokenRe return client.CancelToken(ctx, in, opts...) } -// CancelTokenByUID 取消 Token (取消這個用戶從不同 Device 登入的所有 Token),也包含他裡面的 One Time Toke -func (m *defaultTokenService) CancelTokenByUID(ctx context.Context, in *DoTokenByUIDReq, opts ...grpc.CallOption) (*OKResp, error) { - client := permission.NewTokenServiceClient(m.cli.Conn()) - return client.CancelTokenByUID(ctx, in, opts...) -} - -// CancelTokenByDeviceID 取消 Token -func (m *defaultTokenService) CancelTokenByDeviceID(ctx context.Context, in *DoTokenByDeviceIDReq, opts ...grpc.CallOption) (*OKResp, error) { - client := permission.NewTokenServiceClient(m.cli.Conn()) - return client.CancelTokenByDeviceID(ctx, in, opts...) -} - // ValidationToken 驗證這個 Token 有沒有效 func (m *defaultTokenService) ValidationToken(ctx context.Context, in *ValidationTokenReq, opts ...grpc.CallOption) (*ValidationTokenResp, error) { client := permission.NewTokenServiceClient(m.cli.Conn()) return client.ValidationToken(ctx, in, opts...) } -// GetUserTokensByDeviceIDs 取得目前所對應的 DeviceID 所存在的 Tokens -func (m *defaultTokenService) GetUserTokensByDeviceID(ctx context.Context, in *DoTokenByDeviceIDReq, opts ...grpc.CallOption) (*Tokens, error) { +// CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出 +func (m *defaultTokenService) CancelTokens(ctx context.Context, in *DoTokenByUIDReq, opts ...grpc.CallOption) (*OKResp, error) { client := permission.NewTokenServiceClient(m.cli.Conn()) - return client.GetUserTokensByDeviceID(ctx, in, opts...) + return client.CancelTokens(ctx, in, opts...) } -// GetUserTokensByUID 取得目前所對應的 UID 所存在的 Tokens -func (m *defaultTokenService) GetUserTokensByUID(ctx context.Context, in *DoTokenByUIDReq, opts ...grpc.CallOption) (*Tokens, error) { +// CancelTokenByDeviceId 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token +func (m *defaultTokenService) CancelTokenByDeviceId(ctx context.Context, in *DoTokenByDeviceIDReq, opts ...grpc.CallOption) (*OKResp, error) { client := permission.NewTokenServiceClient(m.cli.Conn()) - return client.GetUserTokensByUID(ctx, in, opts...) + return client.CancelTokenByDeviceId(ctx, in, opts...) +} + +// GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens +func (m *defaultTokenService) GetUserTokensByDeviceId(ctx context.Context, in *DoTokenByDeviceIDReq, opts ...grpc.CallOption) (*Tokens, error) { + client := permission.NewTokenServiceClient(m.cli.Conn()) + return client.GetUserTokensByDeviceId(ctx, in, opts...) +} + +// GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens +func (m *defaultTokenService) GetUserTokensByUid(ctx context.Context, in *QueryTokenByUIDReq, opts ...grpc.CallOption) (*Tokens, error) { + client := permission.NewTokenServiceClient(m.cli.Conn()) + return client.GetUserTokensByUid(ctx, in, opts...) } // NewOneTimeToken 建立一次性使用,例如:RefreshToken @@ -117,7 +119,7 @@ func (m *defaultTokenService) NewOneTimeToken(ctx context.Context, in *CreateOne } // CancelOneTimeToken 取消一次性使用 -func (m *defaultTokenService) CancelOneTimeToken(ctx context.Context, in *CreateOneTimeTokenReq, opts ...grpc.CallOption) (*CreateOneTimeTokenResp, error) { +func (m *defaultTokenService) CancelOneTimeToken(ctx context.Context, in *CancelOneTimeTokenReq, opts ...grpc.CallOption) (*OKResp, error) { client := permission.NewTokenServiceClient(m.cli.Conn()) return client.CancelOneTimeToken(ctx, in, opts...) } diff --git a/etc/permission_example.yaml b/etc/permission_example.yaml new file mode 100644 index 0000000..b1f1d42 --- /dev/null +++ b/etc/permission_example.yaml @@ -0,0 +1,15 @@ +Name: permission.rpc +ListenOn: 0.0.0.0:8080 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: permission.rpc + +RedisCluster: + Host: 127.0.0.1:7001 + Type: cluster + +Token: + Expired: 300 + RefreshExpires: 86500 + Secret: gg88g88 \ No newline at end of file diff --git a/generate/database/mysql/create/20230529020000_create_schema.down.sql b/generate/database/mysql/create/20230529020000_create_schema.down.sql index e7727a5..dc0bfe4 100644 --- a/generate/database/mysql/create/20230529020000_create_schema.down.sql +++ b/generate/database/mysql/create/20230529020000_create_schema.down.sql @@ -1 +1 @@ -DROP DATABASE IF EXISTS `ark_member`; \ No newline at end of file +DROP DATABASE IF EXISTS `ark_permission`; \ No newline at end of file diff --git a/generate/database/mysql/create/20230529020000_create_schema.up.sql b/generate/database/mysql/create/20230529020000_create_schema.up.sql index d997e04..686ffdf 100644 --- a/generate/database/mysql/create/20230529020000_create_schema.up.sql +++ b/generate/database/mysql/create/20230529020000_create_schema.up.sql @@ -1 +1 @@ -CREATE DATABASE IF NOT EXISTS `ark_member`; \ No newline at end of file +CREATE DATABASE IF NOT EXISTS `ark_permission`; \ No newline at end of file diff --git a/generate/protobuf/permission.proto b/generate/protobuf/permission.proto index 83db18c..e6ed508 100644 --- a/generate/protobuf/permission.proto +++ b/generate/protobuf/permission.proto @@ -68,7 +68,13 @@ message CancelTokenReq { // CancelTokenReq 註銷這個 Token message DoTokenByUIDReq { - repeated string uid = 1; + repeated string ids = 1; + string uid = 2; +} + +// QueryTokenByUIDReq 拿這個UID 找 Token +message QueryTokenByUIDReq { + string uid = 1; } // ValidationTokenReq 驗證這個 Token @@ -108,10 +114,14 @@ message Token { // DoTokenByDeviceIDReq 用DeviceID 來做事的 message DoTokenByDeviceIDReq { - repeated string device_id = 1; + string device_id = 1; } message Tokens{ + repeated TokenResp token = 1; +} + +message CancelOneTimeTokenReq { repeated string token = 1; } @@ -125,23 +135,29 @@ service TokenService { rpc RefreshToken(RefreshTokenReq) returns(RefreshTokenResp); // CancelToken 取消 Token,也包含他裡面的 One Time Toke rpc CancelToken(CancelTokenReq) returns(OKResp); - // CancelTokenByUID 取消 Token (取消這個用戶從不同 Device 登入的所有 Token),也包含他裡面的 One Time Toke - rpc CancelTokenByUID(DoTokenByUIDReq) returns(OKResp); - // CancelTokenByDeviceID 取消 Token - rpc CancelTokenByDeviceID(DoTokenByDeviceIDReq) returns(OKResp); // ValidationToken 驗證這個 Token 有沒有效 rpc ValidationToken(ValidationTokenReq) returns(ValidationTokenResp); - // GetUserTokensByDeviceIDs 取得目前所對應的 DeviceID 所存在的 Tokens - rpc GetUserTokensByDeviceID(DoTokenByDeviceIDReq) returns(Tokens); - // GetUserTokensByUID 取得目前所對應的 UID 所存在的 Tokens - rpc GetUserTokensByUID(DoTokenByUIDReq) returns(Tokens); + // CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出 + rpc CancelTokens(DoTokenByUIDReq) returns(OKResp); + + // CancelTokenByDeviceId 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token + rpc CancelTokenByDeviceId(DoTokenByDeviceIDReq) returns(OKResp); + // GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens + rpc GetUserTokensByDeviceId(DoTokenByDeviceIDReq) returns(Tokens); + + + // GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens + rpc GetUserTokensByUid(QueryTokenByUIDReq) returns(Tokens); + // NewOneTimeToken 建立一次性使用,例如:RefreshToken rpc NewOneTimeToken(CreateOneTimeTokenReq) returns(CreateOneTimeTokenResp); // CancelOneTimeToken 取消一次性使用 - rpc CancelOneTimeToken(CreateOneTimeTokenReq) returns(CreateOneTimeTokenResp); + rpc CancelOneTimeToken(CancelOneTimeTokenReq) returns(OKResp); } -//service Role_Service {} -// -//service Permission_Service {} \ No newline at end of file +service RoleService { + rpc Ping(OKResp) returns(OKResp); +} + +service PermissionService {} \ No newline at end of file diff --git a/go.mod b/go.mod index 357d382..25a35bc 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,12 @@ module ark-permission go 1.22.3 require ( + code.30cm.net/wanderland/library-go/errors v1.0.1 + github.com/go-playground/validator/v10 v10.22.0 + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/google/uuid v1.6.0 github.com/zeromicro/go-zero v1.7.0 + go.uber.org/mock v0.4.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 ) @@ -18,21 +23,24 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -65,6 +73,7 @@ require ( go.uber.org/automaxprocs v1.5.3 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sys v0.22.0 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index c1f85b9..6cd58e1 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,17 @@ package config -import "github.com/zeromicro/go-zero/zrpc" +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" + "time" +) type Config struct { zrpc.RpcServerConf + RedisCluster redis.RedisConf + Token struct { + RefreshExpires time.Duration + Expired time.Duration + Secret string + } } diff --git a/internal/domain/const.go b/internal/domain/const.go new file mode 100644 index 0000000..82ac1b8 --- /dev/null +++ b/internal/domain/const.go @@ -0,0 +1,18 @@ +package domain + +type GrantType string + +const ( + PasswordCredentials GrantType = "password" + ClientCredentials GrantType = "client_credentials" + Refreshing GrantType = "refresh_token" +) + +const ( + // DefaultRole 預設role + DefaultRole = "user" +) + +const ( + TokenTypeBearer = "Bearer" +) diff --git a/internal/domain/errors.go b/internal/domain/errors.go new file mode 100644 index 0000000..3182133 --- /dev/null +++ b/internal/domain/errors.go @@ -0,0 +1,65 @@ +package domain + +import ( + mts "ark-permission/internal/lib/metric" + + ers "code.30cm.net/wanderland/library-go/errors" + "code.30cm.net/wanderland/library-go/errors/code" +) + +// 12 represents Scope +// 100 represents Category +// 9 represents Detail error code +// full code 12009 只會有 系統以及錯誤碼,category 是給系統判定用的 +// 目前 Scope 以及分類要系統共用,係向的錯誤各自服務實作就好 + +const ( + TokenUnexpectedSigningErrorCode = iota + 1 + TokenValidateErrorCode + TokenClaimErrorCode +) + +const ( + RedisDelErrorCode = iota + 20 + RedisPipLineErrorCode + RedisErrorCode +) + +// TokenUnexpectedSigningErr 30001 Token 簽名錯誤 +func TokenUnexpectedSigningErr(msg string) *ers.Err { + mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg) +} + +// TokenTokenValidateErr 30002 Token 驗證錯誤 +func TokenTokenValidateErr(msg string) *ers.Err { + mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg) +} + +// TokenClaimError 30003 Token 驗證錯誤 +func TokenClaimError(msg string) *ers.Err { + mts.AppErrorMetrics.AddFailure("token", "token_claim_error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg) +} + +// RedisDelError 30020 Redis 刪除錯誤 +func RedisDelError(msg string) *ers.Err { + // 看需要建立哪些 Metrics + mts.AppErrorMetrics.AddFailure("redis", "del_error") + return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg) +} + +// RedisPipLineError 30021 Redis PipLine 錯誤 +func RedisPipLineError(msg string) *ers.Err { + // 看需要建立哪些 Metrics + mts.AppErrorMetrics.AddFailure("redis", "pip_line_error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg) +} + +// RedisError 30022 Redis 錯誤 +func RedisError(msg string) *ers.Err { + // 看需要建立哪些 Metrics + mts.AppErrorMetrics.AddFailure("redis", "error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg) +} diff --git a/internal/domain/redis.go b/internal/domain/redis.go new file mode 100644 index 0000000..2e31a91 --- /dev/null +++ b/internal/domain/redis.go @@ -0,0 +1,44 @@ +package domain + +import "strings" + +const ( + TicketKeyPrefix = "tic/" +) + +const ( + ClientDataKey = "permission:clients" +) + +type RedisKey string + +const ( + AccessTokenRedisKey RedisKey = "access_token" + RefreshTokenRedisKey RedisKey = "refresh_token" + DeviceTokenRedisKey RedisKey = "device_token" + UIDTokenRedisKey RedisKey = "uid_token" + TicketRedisKey RedisKey = "ticket" + DeviceUIDRedisKey RedisKey = "device_uid" +) + +func (key RedisKey) ToString() string { + return "permission:" + string(key) +} + +func (key RedisKey) With(s ...string) RedisKey { + parts := append([]string{string(key)}, s...) + + return RedisKey(strings.Join(parts, ":")) +} + +func GetAccessTokenRedisKey(id string) string { + return AccessTokenRedisKey.With(id).ToString() +} + +func GetUIDTokenRedisKey(uid string) string { + return UIDTokenRedisKey.With(uid).ToString() +} + +func GetTicketRedisKey(ticket string) string { + return TicketRedisKey.With(ticket).ToString() +} diff --git a/internal/domain/repository/token.go b/internal/domain/repository/token.go new file mode 100644 index 0000000..5f02b98 --- /dev/null +++ b/internal/domain/repository/token.go @@ -0,0 +1,33 @@ +package repository + +import ( + "ark-permission/internal/entity" + "context" + "time" +) + +// TokenRepository token 的 redis 操作 +type TokenRepository interface { + // Create 建立Token + Create(ctx context.Context, token entity.Token) error + // CreateOneTimeToken 建立臨時 Token + CreateOneTimeToken(ctx context.Context, key string, ticket entity.Ticket, dt time.Duration) error + + GetAccessTokenByByOneTimeToken(ctx context.Context, oneTimeToken string) (entity.Token, error) + GetAccessTokenByID(ctx context.Context, id string) (entity.Token, error) + GetAccessTokensByUID(ctx context.Context, uid string) ([]entity.Token, error) + GetAccessTokenCountByUID(uid string) (int, error) + GetAccessTokensByDeviceID(ctx context.Context, deviceID string) ([]entity.Token, error) + GetAccessTokenCountByDeviceID(deviceID string) (int, error) + + Delete(ctx context.Context, token entity.Token) error + DeleteOneTimeToken(ctx context.Context, ids []string, tokens []entity.Token) error + DeleteAccessTokenByID(ctx context.Context, ids []string) error + DeleteAccessTokensByUID(ctx context.Context, uid string) error + DeleteAccessTokensByDeviceID(ctx context.Context, deviceID string) error +} + +type DeviceToken struct { + DeviceID string + TokenID string +} diff --git a/internal/entity/claims.go b/internal/entity/claims.go new file mode 100644 index 0000000..9271795 --- /dev/null +++ b/internal/entity/claims.go @@ -0,0 +1,8 @@ +package entity + +import "github.com/golang-jwt/jwt/v4" + +type Claims struct { + jwt.RegisteredClaims + Data interface{} `json:"data"` +} diff --git a/internal/entity/token.go b/internal/entity/token.go new file mode 100644 index 0000000..791395a --- /dev/null +++ b/internal/entity/token.go @@ -0,0 +1,50 @@ +package entity + +import "time" + +type Token struct { + ID string `json:"id"` + UID string `json:"uid"` + DeviceID string `json:"device_id"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + AccessCreateAt time.Time `json:"access_create_at"` + RefreshToken string `json:"refresh_token"` + RefreshExpiresIn int `json:"refresh_expires_in"` + RefreshCreateAt time.Time `json:"refresh_create_at"` +} + +func (t *Token) AccessTokenExpires() time.Duration { + return time.Duration(t.ExpiresIn) * time.Second +} + +func (t *Token) RefreshTokenExpires() time.Duration { + return time.Duration(t.RefreshExpiresIn) * time.Second +} + +func (t *Token) RefreshTokenExpiresUnix() int64 { + return time.Now().Add(t.RefreshTokenExpires()).Unix() +} + +func (t *Token) IsExpires() bool { + return t.AccessCreateAt.Add(t.AccessTokenExpires()).Before(time.Now()) +} + +func (t *Token) RedisExpiredSec() int64 { + sec := time.Unix(int64(t.ExpiresIn), 0).Sub(time.Now().UTC()) + + return int64(sec.Seconds()) +} + +func (t *Token) RedisRefreshExpiredSec() int64 { + sec := time.Unix(int64(t.RefreshExpiresIn), 0).Sub(time.Now().UTC()) + + return int64(sec.Seconds()) +} + +type UIDToken map[string]int64 + +type Ticket struct { + Data any `json:"data"` + Token Token `json:"token"` +} diff --git a/internal/lib/metric/app.go b/internal/lib/metric/app.go new file mode 100644 index 0000000..59da7ef --- /dev/null +++ b/internal/lib/metric/app.go @@ -0,0 +1,30 @@ +package metric + +import ( + "github.com/zeromicro/go-zero/core/metric" +) + +var AppErrorMetrics = NewAppErrMetrics() + +type appErrMetrics struct { + metric.CounterVec +} + +type Metrics interface { + AddFailure(source, reason string) +} + +// NewAppErrMetrics initiate metrics and register to prometheus +func NewAppErrMetrics() Metrics { + return &appErrMetrics{metric.NewCounterVec(&metric.CounterVecOpts{ + Namespace: "ark", + Subsystem: "permission", + Name: "permission_app_error_total", + Help: "App defined failure total.", + Labels: []string{"source", "reason"}, + })} +} + +func (m *appErrMetrics) AddFailure(source, reason string) { + m.Inc(source, reason) +} diff --git a/internal/lib/metric/db.go b/internal/lib/metric/db.go new file mode 100644 index 0000000..0bad30a --- /dev/null +++ b/internal/lib/metric/db.go @@ -0,0 +1 @@ +package metric diff --git a/internal/lib/middleware/with_context.go b/internal/lib/middleware/with_context.go new file mode 100644 index 0000000..df06757 --- /dev/null +++ b/internal/lib/middleware/with_context.go @@ -0,0 +1,28 @@ +package middleware + +import ( + ers "ark-permission/internal/lib/error" + "context" + "errors" + "time" + + "github.com/zeromicro/go-zero/core/logx" + "google.golang.org/grpc" +) + +const defaultTimeout = 30 * time.Second + +func TimeoutMiddleware(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + + newCtx, cancelCtx := context.WithTimeout(ctx, defaultTimeout) + defer func() { + cancelCtx() + + if errors.Is(newCtx.Err(), context.DeadlineExceeded) { + err = ers.SystemTimeoutError(info.FullMethod) + logx.Errorf("Method: %s, request %v, timeout: %d", info.FullMethod, req, defaultTimeout) + } + }() + + return handler(ctx, req) +} diff --git a/internal/lib/required/validate.go b/internal/lib/required/validate.go new file mode 100644 index 0000000..6fe9a11 --- /dev/null +++ b/internal/lib/required/validate.go @@ -0,0 +1,51 @@ +package required + +import ( + "fmt" + + "github.com/zeromicro/go-zero/core/logx" + + "github.com/go-playground/validator/v10" +) + +type Validate interface { + ValidateAll(obj any) error + BindToValidator(opts ...Option) error +} + +type Validator struct { + V *validator.Validate +} + +// ValidateAll TODO 要移到common 包 +func (v *Validator) ValidateAll(obj any) error { + err := v.V.Struct(obj) + if err != nil { + return err + } + + return nil +} + +func (v *Validator) BindToValidator(opts ...Option) error { + for _, item := range opts { + err := v.V.RegisterValidation(item.ValidatorName, item.ValidatorFunc) + if err != nil { + return fmt.Errorf("failed to register validator : %w", err) + } + } + + return nil +} + +func MustValidator(option ...Option) Validate { + v := &Validator{ + V: validator.New(), + } + + if err := v.BindToValidator(option...); err != nil { + logx.Error("failed to bind validator") + } + + return v +} diff --git a/internal/lib/required/validate_option.go b/internal/lib/required/validate_option.go new file mode 100644 index 0000000..8a896cb --- /dev/null +++ b/internal/lib/required/validate_option.go @@ -0,0 +1,29 @@ +package required + +import ( + "regexp" + + "github.com/go-playground/validator/v10" +) + +type Option struct { + ValidatorName string + ValidatorFunc func(fl validator.FieldLevel) bool +} + +// WithAccount 創建一個新的 Option 結構,包含自定義的驗證函數,用於驗證 email 和台灣的手機號碼格式 +func WithAccount(tagName string) Option { + return Option{ + ValidatorName: tagName, + ValidatorFunc: func(fl validator.FieldLevel) bool { + value := fl.Field().String() + emailRegex := `^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$` + phoneRegex := `^(\+886|0)?9\d{8}$` + + emailMatch, _ := regexp.MatchString(emailRegex, value) + phoneMatch, _ := regexp.MatchString(phoneRegex, value) + + return emailMatch || phoneMatch + }, + } +} diff --git a/internal/logic/cancel_token_by_device_i_d_logic.go b/internal/logic/cancel_token_by_device_i_d_logic.go deleted file mode 100644 index 222a7bd..0000000 --- a/internal/logic/cancel_token_by_device_i_d_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type CancelTokenByDeviceIDLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewCancelTokenByDeviceIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelTokenByDeviceIDLogic { - return &CancelTokenByDeviceIDLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// CancelTokenByDeviceID 取消 Token -func (l *CancelTokenByDeviceIDLogic) CancelTokenByDeviceID(in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) { - // todo: add your logic here and delete this line - - return &permission.OKResp{}, nil -} diff --git a/internal/logic/cancel_token_by_u_i_d_logic.go b/internal/logic/cancel_token_by_u_i_d_logic.go deleted file mode 100644 index 8d6a001..0000000 --- a/internal/logic/cancel_token_by_u_i_d_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type CancelTokenByUIDLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewCancelTokenByUIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelTokenByUIDLogic { - return &CancelTokenByUIDLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// CancelTokenByUID 取消 Token (取消這個用戶從不同 Device 登入的所有 Token),也包含他裡面的 One Time Toke -func (l *CancelTokenByUIDLogic) CancelTokenByUID(in *permission.DoTokenByUIDReq) (*permission.OKResp, error) { - // todo: add your logic here and delete this line - - return &permission.OKResp{}, nil -} diff --git a/internal/logic/get_user_tokens_by_device_i_d_logic.go b/internal/logic/get_user_tokens_by_device_i_d_logic.go deleted file mode 100644 index ea6cd3d..0000000 --- a/internal/logic/get_user_tokens_by_device_i_d_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetUserTokensByDeviceIDLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewGetUserTokensByDeviceIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTokensByDeviceIDLogic { - return &GetUserTokensByDeviceIDLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// GetUserTokensByDeviceIDs 取得目前所對應的 DeviceID 所存在的 Tokens -func (l *GetUserTokensByDeviceIDLogic) GetUserTokensByDeviceID(in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) { - // todo: add your logic here and delete this line - - return &permission.Tokens{}, nil -} diff --git a/internal/logic/get_user_tokens_by_u_i_d_logic.go b/internal/logic/get_user_tokens_by_u_i_d_logic.go deleted file mode 100644 index 339eeb7..0000000 --- a/internal/logic/get_user_tokens_by_u_i_d_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetUserTokensByUIDLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewGetUserTokensByUIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTokensByUIDLogic { - return &GetUserTokensByUIDLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// GetUserTokensByUID 取得目前所對應的 UID 所存在的 Tokens -func (l *GetUserTokensByUIDLogic) GetUserTokensByUID(in *permission.DoTokenByUIDReq) (*permission.Tokens, error) { - // todo: add your logic here and delete this line - - return &permission.Tokens{}, nil -} diff --git a/internal/logic/new_one_time_token_logic.go b/internal/logic/new_one_time_token_logic.go deleted file mode 100644 index 7183ba4..0000000 --- a/internal/logic/new_one_time_token_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type NewOneTimeTokenLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewNewOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewOneTimeTokenLogic { - return &NewOneTimeTokenLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// NewOneTimeToken 建立一次性使用,例如:RefreshToken -func (l *NewOneTimeTokenLogic) NewOneTimeToken(in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) { - // todo: add your logic here and delete this line - - return &permission.CreateOneTimeTokenResp{}, nil -} diff --git a/internal/logic/new_token_logic.go b/internal/logic/new_token_logic.go deleted file mode 100644 index 180eca1..0000000 --- a/internal/logic/new_token_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type NewTokenLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewNewTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewTokenLogic { - return &NewTokenLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// NewToken 建立一個新的 Token,例如:AccessToken -func (l *NewTokenLogic) NewToken(in *permission.AuthorizationReq) (*permission.TokenResp, error) { - // todo: add your logic here and delete this line - - return &permission.TokenResp{}, nil -} diff --git a/internal/logic/refresh_token_logic.go b/internal/logic/refresh_token_logic.go deleted file mode 100644 index 4caef16..0000000 --- a/internal/logic/refresh_token_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type RefreshTokenLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshTokenLogic { - return &RefreshTokenLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// RefreshToken 更新目前的token 以及裡面包含的一次性 Token -func (l *RefreshTokenLogic) RefreshToken(in *permission.RefreshTokenReq) (*permission.RefreshTokenResp, error) { - // todo: add your logic here and delete this line - - return &permission.RefreshTokenResp{}, nil -} diff --git a/internal/logic/cancel_token_logic.go b/internal/logic/roleservice/ping_logic.go similarity index 52% rename from internal/logic/cancel_token_logic.go rename to internal/logic/roleservice/ping_logic.go index 0601e5a..bbe1ccb 100644 --- a/internal/logic/cancel_token_logic.go +++ b/internal/logic/roleservice/ping_logic.go @@ -1,4 +1,4 @@ -package logic +package roleservicelogic import ( "context" @@ -9,22 +9,21 @@ import ( "github.com/zeromicro/go-zero/core/logx" ) -type CancelTokenLogic struct { +type PingLogic struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } -func NewCancelTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelTokenLogic { - return &CancelTokenLogic{ +func NewPingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PingLogic { + return &PingLogic{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } -// CancelToken 取消 Token,也包含他裡面的 One Time Toke -func (l *CancelTokenLogic) CancelToken(in *permission.CancelTokenReq) (*permission.OKResp, error) { +func (l *PingLogic) Ping(in *permission.OKResp) (*permission.OKResp, error) { // todo: add your logic here and delete this line return &permission.OKResp{}, nil diff --git a/internal/logic/cancel_one_time_token_logic.go b/internal/logic/tokenservice/cancel_one_time_token_logic.go similarity index 51% rename from internal/logic/cancel_one_time_token_logic.go rename to internal/logic/tokenservice/cancel_one_time_token_logic.go index 86e6d5e..86092eb 100644 --- a/internal/logic/cancel_one_time_token_logic.go +++ b/internal/logic/tokenservice/cancel_one_time_token_logic.go @@ -1,6 +1,7 @@ -package logic +package tokenservicelogic import ( + ers "code.30cm.net/wanderland/library-go/errors" "context" "ark-permission/gen_result/pb/permission" @@ -23,9 +24,23 @@ func NewCancelOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) } } -// CancelOneTimeToken 取消一次性使用 -func (l *CancelOneTimeTokenLogic) CancelOneTimeToken(in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) { - // todo: add your logic here and delete this line - - return &permission.CreateOneTimeTokenResp{}, nil +type cancelOneTimeTokenReq struct { + Token []string `json:"token" validate:"required"` +} + +// CancelOneTimeToken 取消一次性使用 +func (l *CancelOneTimeTokenLogic) CancelOneTimeToken(in *permission.CancelOneTimeTokenReq) (*permission.OKResp, error) { + // 驗證所需 + if err := l.svcCtx.Validate.ValidateAll(&cancelOneTimeTokenReq{ + Token: in.GetToken(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + err := l.svcCtx.TokenRedisRepo.DeleteOneTimeToken(l.ctx, in.GetToken(), nil) + if err != nil { + return nil, err + } + + return &permission.OKResp{}, nil } diff --git a/internal/logic/tokenservice/cancel_token_by_device_id_logic.go b/internal/logic/tokenservice/cancel_token_by_device_id_logic.go new file mode 100644 index 0000000..e726a26 --- /dev/null +++ b/internal/logic/tokenservice/cancel_token_by_device_id_logic.go @@ -0,0 +1,44 @@ +package tokenservicelogic + +import ( + ers "code.30cm.net/wanderland/library-go/errors" + "context" + + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CancelTokenByDeviceIdLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCancelTokenByDeviceIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelTokenByDeviceIdLogic { + return &CancelTokenByDeviceIdLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// CancelTokenByDeviceId 取消 Token +func (l *CancelTokenByDeviceIdLogic) CancelTokenByDeviceId(in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) { + if err := l.svcCtx.Validate.ValidateAll(&getUserTokensByDeviceIdReq{ + DeviceID: in.GetDeviceId(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + err := l.svcCtx.TokenRedisRepo.DeleteAccessTokensByDeviceID(l.ctx, in.GetDeviceId()) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.DeleteAccessTokensByDeviceID"), + logx.Field("DeviceID", in.GetDeviceId()), + ).Error(err.Error()) + return nil, err + } + return &permission.OKResp{}, nil +} diff --git a/internal/logic/tokenservice/cancel_token_logic.go b/internal/logic/tokenservice/cancel_token_logic.go new file mode 100644 index 0000000..368f297 --- /dev/null +++ b/internal/logic/tokenservice/cancel_token_logic.go @@ -0,0 +1,65 @@ +package tokenservicelogic + +import ( + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/svc" + ers "code.30cm.net/wanderland/library-go/errors" + "context" + "github.com/zeromicro/go-zero/core/logx" +) + +type CancelTokenLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCancelTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelTokenLogic { + return &CancelTokenLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type cancelTokenReq struct { + Token string `json:"token" validate:"required"` +} + +// CancelToken 取消 Token,也包含他裡面的 One Time Toke +func (l *CancelTokenLogic) CancelToken(in *permission.CancelTokenReq) (*permission.OKResp, error) { + // 驗證所需 + if err := l.svcCtx.Validate.ValidateAll(&cancelTokenReq{ + Token: in.GetToken(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + claims, err := parseClaims(in.GetToken(), l.svcCtx.Config.Token.Secret, false) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "parseClaims"), + ).Error(err.Error()) + return nil, err + } + + token, err := l.svcCtx.TokenRedisRepo.GetAccessTokenByID(l.ctx, claims.ID()) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.GetByAccess"), + logx.Field("claims", claims), + ).Error(err.Error()) + return nil, err + } + + err = l.svcCtx.TokenRedisRepo.Delete(l.ctx, token) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.Delete"), + logx.Field("req", token), + ).Error(err.Error()) + return nil, err + } + + return &permission.OKResp{}, nil +} diff --git a/internal/logic/tokenservice/cancel_tokens_logic.go b/internal/logic/tokenservice/cancel_tokens_logic.go new file mode 100644 index 0000000..01fac22 --- /dev/null +++ b/internal/logic/tokenservice/cancel_tokens_logic.go @@ -0,0 +1,52 @@ +package tokenservicelogic + +import ( + ers "code.30cm.net/wanderland/library-go/errors" + "context" + + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CancelTokensLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCancelTokensLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelTokensLogic { + return &CancelTokensLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出 +func (l *CancelTokensLogic) CancelTokens(in *permission.DoTokenByUIDReq) (*permission.OKResp, error) { + if in.GetUid() != "" { + err := l.svcCtx.TokenRedisRepo.DeleteAccessTokensByUID(l.ctx, in.GetUid()) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.DeleteAccessTokensByUID"), + logx.Field("uid", in.GetUid()), + ).Error(err.Error()) + return nil, ers.ResourceInsufficient(err.Error()) + } + } + + if len(in.GetIds()) > 0 { + err := l.svcCtx.TokenRedisRepo.DeleteAccessTokenByID(l.ctx, in.GetIds()) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.DeleteAccessTokenByID"), + logx.Field("ids", in.GetIds()), + ).Error(err.Error()) + return nil, ers.ResourceInsufficient(err.Error()) + } + } + + return &permission.OKResp{}, nil +} diff --git a/internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go b/internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go new file mode 100644 index 0000000..fbc773d --- /dev/null +++ b/internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go @@ -0,0 +1,56 @@ +package tokenservicelogic + +import ( + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/domain" + "ark-permission/internal/svc" + ers "code.30cm.net/wanderland/library-go/errors" + "context" + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserTokensByDeviceIdLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetUserTokensByDeviceIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTokensByDeviceIdLogic { + return &GetUserTokensByDeviceIdLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type getUserTokensByDeviceIdReq struct { + DeviceID string `json:"device_id" validate:"required"` +} + +// GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens +func (l *GetUserTokensByDeviceIdLogic) GetUserTokensByDeviceId(in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) { + if err := l.svcCtx.Validate.ValidateAll(&getUserTokensByDeviceIdReq{ + DeviceID: in.GetDeviceId(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + uidTokens, err := l.svcCtx.TokenRedisRepo.GetAccessTokensByDeviceID(l.ctx, in.GetDeviceId()) + if err != nil { + return nil, err + } + + tokens := make([]*permission.TokenResp, 0, len(uidTokens)) + for _, v := range uidTokens { + tokens = append(tokens, &permission.TokenResp{ + AccessToken: v.AccessToken, + TokenType: domain.TokenTypeBearer, + ExpiresIn: int32(v.ExpiresIn), + RefreshToken: v.RefreshToken, + }) + } + + return &permission.Tokens{ + Token: tokens, + }, nil +} diff --git a/internal/logic/tokenservice/get_user_tokens_by_uid_logic.go b/internal/logic/tokenservice/get_user_tokens_by_uid_logic.go new file mode 100644 index 0000000..9d8616e --- /dev/null +++ b/internal/logic/tokenservice/get_user_tokens_by_uid_logic.go @@ -0,0 +1,56 @@ +package tokenservicelogic + +import ( + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/domain" + "ark-permission/internal/svc" + ers "code.30cm.net/wanderland/library-go/errors" + "context" + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserTokensByUidLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetUserTokensByUidLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTokensByUidLogic { + return &GetUserTokensByUidLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type getUserTokensByUidReq struct { + UID string `json:"uid" validate:"required"` +} + +// GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens +func (l *GetUserTokensByUidLogic) GetUserTokensByUid(in *permission.QueryTokenByUIDReq) (*permission.Tokens, error) { + if err := l.svcCtx.Validate.ValidateAll(&getUserTokensByUidReq{ + UID: in.GetUid(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + uidTokens, err := l.svcCtx.TokenRedisRepo.GetAccessTokensByUID(l.ctx, in.GetUid()) + if err != nil { + return nil, err + } + + tokens := make([]*permission.TokenResp, 0, len(uidTokens)) + for _, v := range uidTokens { + tokens = append(tokens, &permission.TokenResp{ + AccessToken: v.AccessToken, + TokenType: domain.TokenTypeBearer, + ExpiresIn: int32(v.ExpiresIn), + RefreshToken: v.RefreshToken, + }) + } + + return &permission.Tokens{ + Token: tokens, + }, nil +} diff --git a/internal/logic/tokenservice/new_one_time_token_logic.go b/internal/logic/tokenservice/new_one_time_token_logic.go new file mode 100644 index 0000000..389f23a --- /dev/null +++ b/internal/logic/tokenservice/new_one_time_token_logic.go @@ -0,0 +1,70 @@ +package tokenservicelogic + +import ( + "ark-permission/internal/domain" + "ark-permission/internal/entity" + ers "code.30cm.net/wanderland/library-go/errors" + "context" + "github.com/google/uuid" + "time" + + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type NewOneTimeTokenLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewNewOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewOneTimeTokenLogic { + return &NewOneTimeTokenLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// NewOneTimeToken 建立一次性使用,例如:RefreshToken TODO 目前並無後續操作 +func (l *NewOneTimeTokenLogic) NewOneTimeToken(in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) { + // 驗證所需 + if err := l.svcCtx.Validate.ValidateAll(&refreshTokenReq{ + Token: in.GetToken(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + // 驗證Token + claims, err := parseClaims(in.GetToken(), l.svcCtx.Config.Token.Secret, false) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "parseClaims"), + ).Error(err.Error()) + return nil, err + } + + token, err := l.svcCtx.TokenRedisRepo.GetAccessTokenByID(l.ctx, claims.ID()) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.GetByAccess"), + logx.Field("claims", claims), + ).Error(err.Error()) + return nil, err + } + + oneTimeToken := generateRefreshToken(uuid.Must(uuid.NewRandom()).String()) + key := domain.TicketKeyPrefix + oneTimeToken + if err = l.svcCtx.TokenRedisRepo.CreateOneTimeToken(l.ctx, key, entity.Ticket{ + Data: claims, + Token: token, + }, time.Minute); err != nil { + return &permission.CreateOneTimeTokenResp{}, err + } + + return &permission.CreateOneTimeTokenResp{ + OneTimeToken: oneTimeToken, + }, nil +} diff --git a/internal/logic/tokenservice/new_token_logic.go b/internal/logic/tokenservice/new_token_logic.go new file mode 100644 index 0000000..fdccf8f --- /dev/null +++ b/internal/logic/tokenservice/new_token_logic.go @@ -0,0 +1,138 @@ +package tokenservicelogic + +import ( + "ark-permission/internal/config" + "ark-permission/internal/domain" + "ark-permission/internal/entity" + ers "code.30cm.net/wanderland/library-go/errors" + "context" + "github.com/google/uuid" + "time" + + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type NewTokenLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewNewTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewTokenLogic { + return &NewTokenLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3 +type authorizationReq struct { + GrantType domain.GrantType `json:"grant_type" validate:"required,oneof=password client_credentials refresh_token"` + DeviceID string `json:"device_id"` + Scope string `json:"scope" validate:"required"` + Data map[string]string `json:"data"` + Expires int `json:"expires"` + IsRefreshToken bool `json:"is_refresh_token"` +} + +// NewToken 建立一個新的 Token,例如:AccessToken +func (l *NewTokenLogic) NewToken(in *permission.AuthorizationReq) (*permission.TokenResp, error) { + data := authorizationReq{ + GrantType: domain.GrantType(in.GetGrantType()), + Scope: in.GetScope(), + DeviceID: in.GetDeviceId(), + Data: in.GetData(), + Expires: int(in.GetExpires()), + IsRefreshToken: in.GetIsRefreshToken(), + } + // 驗證所需 + if err := l.svcCtx.Validate.ValidateAll(&data); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + token, err := newToken(data, l.svcCtx.Config) + if err != nil { + return nil, err + } + + err = l.svcCtx.TokenRedisRepo.Create(l.ctx, *token) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.Create"), + logx.Field("token", token), + ).Error(err.Error()) + return nil, err + } + + return &permission.TokenResp{ + AccessToken: token.AccessToken, + TokenType: domain.TokenTypeBearer, + ExpiresIn: int32(token.ExpiresIn), + RefreshToken: token.RefreshToken, + }, nil +} + +func newToken(authReq authorizationReq, cfg config.Config) (*entity.Token, error) { + // 準備建立 Token 所需 + now := time.Now().UTC() + expires := authReq.Expires + refreshExpires := authReq.Expires + if expires <= 0 { + // 將時間加上 300 秒 + sec := time.Duration(cfg.Token.Expired.Seconds()) * time.Second + newTime := now.Add(sec) + // 獲取 Unix 時間戳 + timestamp := newTime.Unix() + expires = int(timestamp) + refreshExpires = expires + } + + // 如果這是一個 Refresh Token 過期時間要比普通的Token 長 + if authReq.IsRefreshToken { + // 將時間加上 300 秒 + sec := time.Duration(cfg.Token.RefreshExpires.Seconds()) * time.Second + newTime := now.Add(sec) + // 獲取 Unix 時間戳 + timestamp := newTime.Unix() + refreshExpires = int(timestamp) + } + + token := entity.Token{ + ID: uuid.Must(uuid.NewRandom()).String(), + DeviceID: authReq.DeviceID, + ExpiresIn: expires, + RefreshExpiresIn: refreshExpires, + AccessCreateAt: now, + RefreshCreateAt: now, + } + + claims := claims(authReq.Data) + claims.SetRole(domain.DefaultRole) + claims.SetID(token.ID) + claims.SetScope(authReq.Scope) + + token.UID = claims.UID() + + if authReq.DeviceID != "" { + claims.SetDeviceID(authReq.DeviceID) + } + + var err error + token.AccessToken, err = generateAccessTokenFunc(token, claims, cfg.Token.Secret) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "generateAccessTokenFunc"), + logx.Field("claims", claims), + ).Error(err.Error()) + return nil, err + } + + if authReq.IsRefreshToken { + token.RefreshToken = generateRefreshTokenFunc(token.AccessToken) + } + + return &token, nil +} diff --git a/internal/logic/tokenservice/new_token_logic_test.go b/internal/logic/tokenservice/new_token_logic_test.go new file mode 100644 index 0000000..76263c9 --- /dev/null +++ b/internal/logic/tokenservice/new_token_logic_test.go @@ -0,0 +1,199 @@ +package tokenservicelogic + +import ( + "ark-permission/internal/entity" + "github.com/golang-jwt/jwt/v4" + "testing" + "time" +) + +// func TestNewTokenLogic_NewToken(t *testing.T) { +// // mock +// ctrl := gomock.NewController(t) +// defer ctrl.Finish() +// +// tokenMockRepo := repoMock.NewMockTokenRepository(ctrl) +// mockValidate := libMock.NewMockValidate(ctrl) +// +// sc := svc.ServiceContext{ +// TokenRedisRepo: tokenMockRepo, +// Validate: mockValidate, +// } +// +// l := NewNewTokenLogic(context.Background(), &sc) +// +// tests := []struct { +// name string +// input *permission.AuthorizationReq +// setupMocks func() +// expectError bool +// expected *permission.TokenResp +// }{ +// { +// name: "Valid token request", +// input: &permission.AuthorizationReq{ +// GrantType: "authorization_code", +// DeviceId: "device123", +// Scope: "read", +// Expires: 3600, +// IsRefreshToken: false, +// Data: map[string]string{ +// "uid": "user123", +// }, +// }, +// setupMocks: func() { +// mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil) +// tokenMockRepo.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil).Do(func(ctx context.Context, token entity.Token) { +// token.AccessToken = "access_token" +// }) +// generateAccessTokenFunc = func(token entity.Token, data any, sign string) (string, error) { +// return "access_token", nil +// } +// generateRefreshTokenFunc = func(accessToken string) string { +// return "refresh_token" +// } +// }, +// expectError: false, +// expected: &permission.TokenResp{ +// AccessToken: "access_token", +// TokenType: domain.TokenTypeBearer, +// ExpiresIn: 3600, +// RefreshToken: "", +// }, +// }, +// { +// name: "Validation error", +// input: &permission.AuthorizationReq{ +// GrantType: "invalid_grant", +// DeviceId: "device123", +// Scope: "read", +// Expires: 3600, +// IsRefreshToken: false, +// Data: map[string]string{ +// "uid": "user123", +// }, +// }, +// setupMocks: func() { +// mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("invalid grant type")) +// }, +// expectError: true, +// expected: nil, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// tt.setupMocks() +// +// resp, err := l.NewToken(tt.input) +// if tt.expectError { +// assert.Error(t, err) +// } else { +// assert.NoError(t, err) +// assert.Equal(t, tt.expected, resp) +// } +// }) +// } +// } + +// 測試 generateAccessToken 函數 +func TestGenerateAccessToken(t *testing.T) { + // 定義測試用例 + tests := []struct { + name string + token entity.Token + data any + sign string + shouldFail bool + shouldVerify bool + }{ + { + name: "Valid token with admin role", + token: entity.Token{ + ID: "123", + ExpiresIn: int(time.Now().Add(time.Hour * 24).Unix()), + }, + data: map[string]string{"role": "admin"}, + sign: "secret", + shouldFail: false, + shouldVerify: true, + }, + { + name: "Expired token", + token: entity.Token{ + ID: "456", + ExpiresIn: int(time.Now().Add(-time.Hour * 24).Unix()), // 過期時間 + }, + data: map[string]string{"role": "user"}, + sign: "secret", + shouldFail: false, // 這個測試不會失敗,因為過期檢查通常在驗證時進行 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tokenString, err := generateAccessToken(tt.token, tt.data, tt.sign) + if (err != nil) != tt.shouldFail { + t.Errorf("generateAccessToken() error = %v, shouldFail %v", err, tt.shouldFail) + return + } + + if tt.shouldVerify { + // 驗證生成的 token + parsedToken, err := jwt.ParseWithClaims(tokenString, &entity.Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(tt.sign), nil + }) + if err != nil { + t.Errorf("Error parsing token: %v", err) + return + } + + if claims, ok := parsedToken.Claims.(*entity.Claims); ok && parsedToken.Valid { + if claims.ID != tt.token.ID { + t.Errorf("Expected ID %v, got %v", tt.token.ID, claims.ID) + } + if claims.Issuer != "permission" { + t.Errorf("Expected Issuer 'permission', got %v", claims.Issuer) + } + for k, v := range tt.data.(map[string]string) { + if claims.Data.(map[string]any)[k] != v { + t.Errorf("Expected data %v, got %v", v, claims.Data.(map[string]string)[k]) + } + } + } else { + t.Errorf("Invalid token claims") + } + } + }) + } +} + +// 測試 generateRefreshToken 函數 +func TestGenerateRefreshToken(t *testing.T) { + // 定義測試用例 + tests := []struct { + accessToken string + expected string + }{ + { + accessToken: "test_access_token", + expected: "4993552f2cc6c4e57fa5738f9b161a1a4051c8370cddb32514c8f6f4c797801f", + }, + { + accessToken: "another_test_access_token", + expected: "8361833e9a11f829f2be9a00f1939b5a72408ff829451169f3b223c41768cfa2", + }, + { + accessToken: "", + expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + } + + for _, tt := range tests { + t.Run(tt.accessToken, func(t *testing.T) { + got := generateRefreshToken(tt.accessToken) + if got != tt.expected { + t.Errorf("generateRefreshToken(%s) = %s; want %s", tt.accessToken, got, tt.expected) + } + }) + } +} diff --git a/internal/logic/tokenservice/refresh_token_logic.go b/internal/logic/tokenservice/refresh_token_logic.go new file mode 100644 index 0000000..e059b7a --- /dev/null +++ b/internal/logic/tokenservice/refresh_token_logic.go @@ -0,0 +1,106 @@ +package tokenservicelogic + +import ( + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/domain" + "ark-permission/internal/svc" + ers "code.30cm.net/wanderland/library-go/errors" + "context" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RefreshTokenLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshTokenLogic { + return &RefreshTokenLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type refreshReq struct { + RefreshToken string `json:"grant_type" validate:"required"` + DeviceID string `json:"device_id" validate:"required"` + Scope string `json:"scope" validate:"required"` +} + +// RefreshToken 更新目前的token 以及裡面包含的一次性 Token +func (l *RefreshTokenLogic) RefreshToken(in *permission.RefreshTokenReq) (*permission.RefreshTokenResp, error) { + // 驗證所需 + if err := l.svcCtx.Validate.ValidateAll(&refreshReq{ + RefreshToken: in.GetToken(), + Scope: in.GetScope(), + DeviceID: in.GetDeviceId(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + // step 1 拿看看有沒有這個 refresh token + token, err := l.svcCtx.TokenRedisRepo.GetAccessTokenByByOneTimeToken(l.ctx, in.Token) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.GetByRefresh"), + logx.Field("req", in), + ).Error(err.Error()) + return nil, err + } + + // 取得 Data + c, err := parseClaims(token.AccessToken, l.svcCtx.Config.Token.Secret, false) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "parseClaims"), + logx.Field("token", token), + ).Error(err.Error()) + return nil, err + } + + // step 2 建立新 token + nt, err := newToken(authorizationReq{ + GrantType: domain.ClientCredentials, + Scope: in.GetScope(), + DeviceID: in.GetDeviceId(), + Data: c, + Expires: int(in.GetExpires()), + IsRefreshToken: true, + }, l.svcCtx.Config) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "newToken"), + logx.Field("req", in), + ).Error(err.Error()) + return nil, err + } + + // 刪除掉舊的 token + err = l.svcCtx.TokenRedisRepo.Delete(l.ctx, token) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.Delete"), + logx.Field("req", token), + ).Error(err.Error()) + return nil, err + } + + err = l.svcCtx.TokenRedisRepo.Create(l.ctx, *nt) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.Create"), + logx.Field("token", token), + ).Error(err.Error()) + return nil, err + } + + return &permission.RefreshTokenResp{ + Token: nt.AccessToken, + OneTimeToken: nt.RefreshToken, + ExpiresIn: int64(nt.ExpiresIn), + TokenType: domain.TokenTypeBearer, + }, nil +} diff --git a/internal/logic/tokenservice/utils_claims.go b/internal/logic/tokenservice/utils_claims.go new file mode 100644 index 0000000..2d59b66 --- /dev/null +++ b/internal/logic/tokenservice/utils_claims.go @@ -0,0 +1,55 @@ +package tokenservicelogic + +type claims map[string]string + +func (c claims) SetID(id string) { + c["id"] = id +} + +func (c claims) SetRole(role string) { + c["role"] = role +} + +func (c claims) SetDeviceID(deviceID string) { + c["device_id"] = deviceID +} + +func (c claims) SetScope(scope string) { + c["scope"] = scope +} + +func (c claims) Role() string { + role, ok := c["role"] + if !ok { + return "" + } + + return role +} + +func (c claims) ID() string { + id, ok := c["id"] + if !ok { + return "" + } + + return id +} + +func (c claims) DeviceID() string { + deviceID, ok := c["device_id"] + if !ok { + return "" + } + + return deviceID +} + +func (c claims) UID() string { + uid, ok := c["uid"] + if !ok { + return "" + } + + return uid +} diff --git a/internal/logic/tokenservice/utils_jwt.go b/internal/logic/tokenservice/utils_jwt.go new file mode 100644 index 0000000..3ef34fd --- /dev/null +++ b/internal/logic/tokenservice/utils_jwt.go @@ -0,0 +1,104 @@ +package tokenservicelogic + +import ( + "ark-permission/internal/domain" + "ark-permission/internal/entity" + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "github.com/golang-jwt/jwt/v4" + "time" +) + +var generateAccessTokenFunc = generateAccessToken +var generateRefreshTokenFunc = generateRefreshToken + +func generateAccessToken(token entity.Token, data any, sign string) (string, error) { + claim := entity.Claims{ + Data: data, + RegisteredClaims: jwt.RegisteredClaims{ + ID: token.ID, + ExpiresAt: jwt.NewNumericDate(time.Unix(int64(token.ExpiresIn), 0)), + Issuer: "permission", + }, + } + + accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claim). + SignedString([]byte(sign)) + if err != nil { + return "", domain.TokenClaimError(err.Error()) + } + + return accessToken, nil +} + +func generateRefreshToken(accessToken string) string { + buf := bytes.NewBufferString(accessToken) + h := sha256.New() + _, _ = h.Write(buf.Bytes()) + + return hex.EncodeToString(h.Sum(nil)) +} + +func parseToken(accessToken string, secret string, validate bool) (jwt.MapClaims, error) { + // 跳過驗證的解析 + var token *jwt.Token + var err error + + if validate { + token, err = jwt.Parse(accessToken, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, domain.TokenUnexpectedSigningErr(fmt.Sprintf("token unexpected signing method: %v", token.Header["alg"])) + } + return []byte(secret), nil + }) + if err != nil { + return jwt.MapClaims{}, err + } + } else { + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) + token, err = parser.Parse(accessToken, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + if err != nil { + return jwt.MapClaims{}, err + } + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok && token.Valid { + return jwt.MapClaims{}, domain.TokenTokenValidateErr("token valid error") + } + + return claims, nil +} + +func parseClaims(accessToken string, secret string, validate bool) (claims, error) { + claimMap, err := parseToken(accessToken, secret, validate) + if err != nil { + return claims{}, err + } + + claimsData, ok := claimMap["data"].(map[string]any) + if ok { + return convertMap(claimsData), nil + } + + return claims{}, domain.TokenClaimError("get data from claim map error") +} + +func convertMap(input map[string]interface{}) map[string]string { + output := make(map[string]string) + for key, value := range input { + switch v := value.(type) { + case string: + output[key] = v + case fmt.Stringer: + output[key] = v.String() + default: + output[key] = fmt.Sprintf("%v", value) + } + } + return output +} diff --git a/internal/logic/tokenservice/validation_token_logic.go b/internal/logic/tokenservice/validation_token_logic.go new file mode 100644 index 0000000..be6ab1a --- /dev/null +++ b/internal/logic/tokenservice/validation_token_logic.go @@ -0,0 +1,68 @@ +package tokenservicelogic + +import ( + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/svc" + ers "code.30cm.net/wanderland/library-go/errors" + "context" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ValidationTokenLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewValidationTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ValidationTokenLogic { + return &ValidationTokenLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type refreshTokenReq struct { + Token string `json:"token" validate:"required"` +} + +// ValidationToken 驗證這個 Token 有沒有效 +func (l *ValidationTokenLogic) ValidationToken(in *permission.ValidationTokenReq) (*permission.ValidationTokenResp, error) { + // 驗證所需 + if err := l.svcCtx.Validate.ValidateAll(&refreshTokenReq{ + Token: in.GetToken(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + claims, err := parseClaims(in.GetToken(), l.svcCtx.Config.Token.Secret, true) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "parseClaims"), + ).Info(err.Error()) + return nil, err + } + token, err := l.svcCtx.TokenRedisRepo.GetAccessTokenByID(l.ctx, claims.ID()) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "TokenRedisRepo.GetByAccess"), + logx.Field("claims", claims), + ).Error(err.Error()) + return nil, err + } + + return &permission.ValidationTokenResp{ + Token: &permission.Token{ + Id: token.ID, + Uid: token.UID, + DeviceId: token.DeviceID, + AccessCreateAt: token.AccessCreateAt.Unix(), + AccessToken: token.AccessToken, + ExpiresIn: int32(token.ExpiresIn), + RefreshToken: token.RefreshToken, + RefreshExpiresIn: int32(token.RefreshExpiresIn), + RefreshCreateAt: token.RefreshCreateAt.Unix(), + }, + Data: claims, + }, nil +} diff --git a/internal/logic/validation_token_logic.go b/internal/logic/validation_token_logic.go deleted file mode 100644 index 40588d8..0000000 --- a/internal/logic/validation_token_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type ValidationTokenLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewValidationTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ValidationTokenLogic { - return &ValidationTokenLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// ValidationToken 驗證這個 Token 有沒有效 -func (l *ValidationTokenLogic) ValidationToken(in *permission.ValidationTokenReq) (*permission.ValidationTokenResp, error) { - // todo: add your logic here and delete this line - - return &permission.ValidationTokenResp{}, nil -} diff --git a/internal/mock/lib/validate.go b/internal/mock/lib/validate.go new file mode 100644 index 0000000..123fe29 --- /dev/null +++ b/internal/mock/lib/validate.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./validate.go +// +// Generated by this command: +// +// mockgen -source=./validate.go -destination=../../mock/lib/validate.go -package=lib +// + +// Package lib is a generated GoMock package. +package lib + +import ( + required "ark-permission/internal/lib/required" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockValidate is a mock of Validate interface. +type MockValidate struct { + ctrl *gomock.Controller + recorder *MockValidateMockRecorder +} + +// MockValidateMockRecorder is the mock recorder for MockValidate. +type MockValidateMockRecorder struct { + mock *MockValidate +} + +// NewMockValidate creates a new mock instance. +func NewMockValidate(ctrl *gomock.Controller) *MockValidate { + mock := &MockValidate{ctrl: ctrl} + mock.recorder = &MockValidateMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockValidate) EXPECT() *MockValidateMockRecorder { + return m.recorder +} + +// BindToValidator mocks base method. +func (m *MockValidate) BindToValidator(opts ...required.Option) error { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BindToValidator", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// BindToValidator indicates an expected call of BindToValidator. +func (mr *MockValidateMockRecorder) BindToValidator(opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindToValidator", reflect.TypeOf((*MockValidate)(nil).BindToValidator), opts...) +} + +// ValidateAll mocks base method. +func (m *MockValidate) ValidateAll(obj any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateAll", obj) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateAll indicates an expected call of ValidateAll. +func (mr *MockValidateMockRecorder) ValidateAll(obj any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAll", reflect.TypeOf((*MockValidate)(nil).ValidateAll), obj) +} diff --git a/internal/mock/repository/token.go b/internal/mock/repository/token.go new file mode 100644 index 0000000..6ef99eb --- /dev/null +++ b/internal/mock/repository/token.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./token.go +// +// Generated by this command: +// +// mockgen -source=./token.go -destination=../../mock/repository/token.go -package=repository +// + +// Package repository is a generated GoMock package. +package repository + +import ( + entity "ark-permission/internal/entity" + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockTokenRepository is a mock of TokenRepository interface. +type MockTokenRepository struct { + ctrl *gomock.Controller + recorder *MockTokenRepositoryMockRecorder +} + +// MockTokenRepositoryMockRecorder is the mock recorder for MockTokenRepository. +type MockTokenRepositoryMockRecorder struct { + mock *MockTokenRepository +} + +// NewMockTokenRepository creates a new mock instance. +func NewMockTokenRepository(ctrl *gomock.Controller) *MockTokenRepository { + mock := &MockTokenRepository{ctrl: ctrl} + mock.recorder = &MockTokenRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTokenRepository) EXPECT() *MockTokenRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockTokenRepository) Create(ctx context.Context, token entity.Token) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, token) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockTokenRepositoryMockRecorder) Create(ctx, token any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockTokenRepository)(nil).Create), ctx, token) +} diff --git a/internal/repository/token.go b/internal/repository/token.go new file mode 100644 index 0000000..d91f73c --- /dev/null +++ b/internal/repository/token.go @@ -0,0 +1,301 @@ +package repository + +import ( + "ark-permission/internal/domain" + "ark-permission/internal/domain/repository" + "ark-permission/internal/entity" + ers "code.30cm.net/wanderland/library-go/errors" + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/zeromicro/go-zero/core/stores/redis" +) + +type TokenRepositoryParam struct { + Store *redis.Redis `name:"redis"` +} + +type tokenRepository struct { + store *redis.Redis +} + +func NewTokenRepository(param TokenRepositoryParam) repository.TokenRepository { + return &tokenRepository{ + store: param.Store, + } +} + +func (t *tokenRepository) Create(ctx context.Context, token entity.Token) error { + body, err := json.Marshal(token) + if err != nil { + return ers.ArkInternal("json.Marshal token error", err.Error()) + } + if err := t.store.Pipelined(func(tx redis.Pipeliner) error { + refreshTTL := time.Duration(token.RedisRefreshExpiredSec()) * time.Second + + if err := t.setToken(ctx, tx, token, body, refreshTTL); err != nil { + return err + } + + if err := t.setRefreshToken(ctx, tx, token, refreshTTL); err != nil { + return err + } + + return t.setRelation(ctx, tx, token.UID, token.DeviceID, token.ID, refreshTTL) + }); err != nil { + return domain.RedisPipLineError(err.Error()) + } + + return nil +} + +func (t *tokenRepository) Delete(ctx context.Context, token entity.Token) error { + keys := []string{ + domain.GetAccessTokenRedisKey(token.ID), + domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString(), + } + + if err := t.deleteKeys(ctx, keys...); err != nil { + return domain.RedisPipLineError(err.Error()) + } + + _, _ = t.store.Srem(domain.DeviceTokenRedisKey.With(token.DeviceID).ToString(), token.ID) + _, _ = t.store.Srem(domain.UIDTokenRedisKey.With(token.UID).ToString(), token.ID) + + return nil +} + +func (t *tokenRepository) GetAccessTokenByID(ctx context.Context, id string) (entity.Token, error) { + token, err := t.get(ctx, domain.GetAccessTokenRedisKey(id)) + if err != nil { + return entity.Token{}, err + } + + return token, nil +} + +func (t *tokenRepository) DeleteAccessTokensByUID(ctx context.Context, uid string) error { + tokens, err := t.GetAccessTokensByUID(ctx, uid) + if err != nil { + return err + } + + for _, token := range tokens { + if err := t.Delete(ctx, token); err != nil { + return err + } + } + + return nil +} + +func (t *tokenRepository) DeleteAccessTokenByID(ctx context.Context, ids []string) error { + for _, tokenID := range ids { + token, err := t.GetAccessTokenByID(ctx, tokenID) + if err != nil { + continue + } + + keys := []string{ + domain.GetAccessTokenRedisKey(token.ID), + domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString(), + } + + if err := t.deleteKeys(ctx, keys...); err != nil { + continue + } + + _, _ = t.store.Srem(domain.DeviceTokenRedisKey.With(token.DeviceID).ToString(), token.ID) + _, _ = t.store.Srem(domain.UIDTokenRedisKey.With(token.UID).ToString(), token.ID) + } + + return nil +} + +func (t *tokenRepository) GetAccessTokensByUID(ctx context.Context, uid string) ([]entity.Token, error) { + return t.getTokensBySet(ctx, domain.GetUIDTokenRedisKey(uid)) +} + +func (t *tokenRepository) GetAccessTokensByDeviceID(ctx context.Context, deviceID string) ([]entity.Token, error) { + return t.getTokensBySet(ctx, domain.DeviceTokenRedisKey.With(deviceID).ToString()) +} + +func (t *tokenRepository) DeleteAccessTokensByDeviceID(ctx context.Context, deviceID string) error { + + tokens, err := t.GetAccessTokensByDeviceID(ctx, deviceID) + if err != nil { + return domain.RedisDelError(fmt.Sprintf("GetAccessTokensByDeviceID error: %v", err)) + } + + var keys []string + for _, token := range tokens { + keys = append(keys, domain.GetAccessTokenRedisKey(token.ID)) + keys = append(keys, domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString()) + + } + + err = t.store.Pipelined(func(tx redis.Pipeliner) error { + for _, token := range tokens { + _, _ = t.store.Srem(domain.UIDTokenRedisKey.With(token.UID).ToString(), token.ID) + } + return nil + }) + if err != nil { + return err + } + + if err := t.deleteKeys(ctx, keys...); err != nil { + return err + } + + _, err = t.store.Del(domain.DeviceTokenRedisKey.With(deviceID).ToString()) + return err +} + +func (t *tokenRepository) GetAccessTokenCountByDeviceID(deviceID string) (int, error) { + return t.getCountBySet(domain.DeviceTokenRedisKey.With(deviceID).ToString()) +} + +func (t *tokenRepository) GetAccessTokenCountByUID(uid string) (int, error) { + return t.getCountBySet(domain.UIDTokenRedisKey.With(uid).ToString()) +} + +func (t *tokenRepository) GetAccessTokenByByOneTimeToken(ctx context.Context, oneTimeToken string) (entity.Token, error) { + id, err := t.store.Get(domain.RefreshTokenRedisKey.With(oneTimeToken).ToString()) + if err != nil { + return entity.Token{}, domain.RedisError(fmt.Sprintf("GetAccessTokenByByOneTimeToken store.Get error: %s", err.Error())) + } + + if id == "" { + return entity.Token{}, ers.ResourceNotFound("token key not found in redis", domain.RefreshTokenRedisKey.With(oneTimeToken).ToString()) + } + + return t.GetAccessTokenByID(ctx, id) +} + +func (t *tokenRepository) DeleteOneTimeToken(ctx context.Context, ids []string, tokens []entity.Token) error { + var keys []string + + for _, id := range ids { + keys = append(keys, domain.RefreshTokenRedisKey.With(id).ToString()) + } + + for _, token := range tokens { + keys = append(keys, domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString()) + } + + return t.deleteKeys(ctx, keys...) +} + +func (t *tokenRepository) CreateOneTimeToken(ctx context.Context, key string, ticket entity.Ticket, expires time.Duration) error { + body, err := json.Marshal(ticket) + if err != nil { + return ers.InvalidFormat("CreateOneTimeToken json.Marshal error", err.Error()) + } + + _, err = t.store.SetnxEx(domain.RefreshTokenRedisKey.With(key).ToString(), string(body), int(expires.Seconds())) + if err != nil { + return domain.RedisError(fmt.Sprintf("CreateOneTimeToken store.SetnxEx error: %s", err.Error())) + } + + return nil +} + +// -------------------- Private area -------------------- + +func (t *tokenRepository) get(ctx context.Context, key string) (entity.Token, error) { + body, err := t.store.GetCtx(ctx, key) + if err != nil { + return entity.Token{}, domain.RedisError(fmt.Sprintf("token %s not found in redis: %s", key, err.Error())) + } + + if body == "" { + return entity.Token{}, ers.ResourceNotFound("this token not found") + } + + var token entity.Token + if err := json.Unmarshal([]byte(body), &token); err != nil { + return entity.Token{}, ers.ArkInternal("json.Unmarshal token error", err.Error()) + } + + return token, nil +} + +func (t *tokenRepository) setToken(ctx context.Context, tx redis.Pipeliner, token entity.Token, body []byte, ttl time.Duration) error { + return tx.Set(ctx, domain.GetAccessTokenRedisKey(token.ID), body, ttl).Err() +} + +func (t *tokenRepository) setRefreshToken(ctx context.Context, tx redis.Pipeliner, token entity.Token, ttl time.Duration) error { + if token.RefreshToken != "" { + return tx.Set(ctx, domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString(), token.ID, ttl).Err() + } + return nil +} + +func (t *tokenRepository) setRelation(ctx context.Context, tx redis.Pipeliner, uid, deviceID, tokenID string, ttl time.Duration) error { + if err := tx.SAdd(ctx, domain.UIDTokenRedisKey.With(uid).ToString(), tokenID).Err(); err != nil { + return err + } + + if err := tx.SAdd(ctx, domain.DeviceTokenRedisKey.With(deviceID).ToString(), tokenID).Err(); err != nil { + return err + } + + return nil +} + +func (t *tokenRepository) deleteKeys(ctx context.Context, keys ...string) error { + return t.store.Pipelined(func(tx redis.Pipeliner) error { + for _, key := range keys { + if err := tx.Del(ctx, key).Err(); err != nil { + return domain.RedisDelError(fmt.Sprintf("store.Del key error: %v", err)) + } + } + return nil + }) +} + +func (t *tokenRepository) getTokensBySet(ctx context.Context, setKey string) ([]entity.Token, error) { + ids, err := t.store.Smembers(setKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, nil + } + return nil, domain.RedisError(fmt.Sprintf("getTokensBySet store.Get %s error: %v", setKey, err.Error())) + } + + var tokens []entity.Token + var deleteTokens []string + now := time.Now().Unix() + for _, id := range ids { + token, err := t.get(ctx, domain.GetAccessTokenRedisKey(id)) + if err != nil { + deleteTokens = append(deleteTokens, id) + continue + } + + if int64(token.ExpiresIn) < now { + deleteTokens = append(deleteTokens, id) + continue + } + + tokens = append(tokens, token) + } + + if len(deleteTokens) > 0 { + _ = t.DeleteAccessTokenByID(ctx, deleteTokens) + } + + return tokens, nil +} + +func (t *tokenRepository) getCountBySet(setKey string) (int, error) { + count, err := t.store.Scard(setKey) + if err != nil { + return 0, err + } + return int(count), nil +} diff --git a/internal/server/permissionservice/permission_service_server.go b/internal/server/permissionservice/permission_service_server.go new file mode 100644 index 0000000..d645530 --- /dev/null +++ b/internal/server/permissionservice/permission_service_server.go @@ -0,0 +1,20 @@ +// Code generated by goctl. DO NOT EDIT. +// Source: permission.proto + +package server + +import ( + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/svc" +) + +type PermissionServiceServer struct { + svcCtx *svc.ServiceContext + permission.UnimplementedPermissionServiceServer +} + +func NewPermissionServiceServer(svcCtx *svc.ServiceContext) *PermissionServiceServer { + return &PermissionServiceServer{ + svcCtx: svcCtx, + } +} diff --git a/internal/server/roleservice/role_service_server.go b/internal/server/roleservice/role_service_server.go new file mode 100644 index 0000000..0175dc7 --- /dev/null +++ b/internal/server/roleservice/role_service_server.go @@ -0,0 +1,28 @@ +// Code generated by goctl. DO NOT EDIT. +// Source: permission.proto + +package server + +import ( + "context" + + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/logic/roleservice" + "ark-permission/internal/svc" +) + +type RoleServiceServer struct { + svcCtx *svc.ServiceContext + permission.UnimplementedRoleServiceServer +} + +func NewRoleServiceServer(svcCtx *svc.ServiceContext) *RoleServiceServer { + return &RoleServiceServer{ + svcCtx: svcCtx, + } +} + +func (s *RoleServiceServer) Ping(ctx context.Context, in *permission.OKResp) (*permission.OKResp, error) { + l := roleservicelogic.NewPingLogic(ctx, s.svcCtx) + return l.Ping(in) +} diff --git a/internal/server/token_service_server.go b/internal/server/tokenservice/token_service_server.go similarity index 51% rename from internal/server/token_service_server.go rename to internal/server/tokenservice/token_service_server.go index 81e5554..c5adb92 100644 --- a/internal/server/token_service_server.go +++ b/internal/server/tokenservice/token_service_server.go @@ -7,7 +7,7 @@ import ( "context" "ark-permission/gen_result/pb/permission" - "ark-permission/internal/logic" + "ark-permission/internal/logic/tokenservice" "ark-permission/internal/svc" ) @@ -24,60 +24,60 @@ func NewTokenServiceServer(svcCtx *svc.ServiceContext) *TokenServiceServer { // NewToken 建立一個新的 Token,例如:AccessToken func (s *TokenServiceServer) NewToken(ctx context.Context, in *permission.AuthorizationReq) (*permission.TokenResp, error) { - l := logic.NewNewTokenLogic(ctx, s.svcCtx) + l := tokenservicelogic.NewNewTokenLogic(ctx, s.svcCtx) return l.NewToken(in) } // RefreshToken 更新目前的token 以及裡面包含的一次性 Token func (s *TokenServiceServer) RefreshToken(ctx context.Context, in *permission.RefreshTokenReq) (*permission.RefreshTokenResp, error) { - l := logic.NewRefreshTokenLogic(ctx, s.svcCtx) + l := tokenservicelogic.NewRefreshTokenLogic(ctx, s.svcCtx) return l.RefreshToken(in) } // CancelToken 取消 Token,也包含他裡面的 One Time Toke func (s *TokenServiceServer) CancelToken(ctx context.Context, in *permission.CancelTokenReq) (*permission.OKResp, error) { - l := logic.NewCancelTokenLogic(ctx, s.svcCtx) + l := tokenservicelogic.NewCancelTokenLogic(ctx, s.svcCtx) return l.CancelToken(in) } -// CancelTokenByUID 取消 Token (取消這個用戶從不同 Device 登入的所有 Token),也包含他裡面的 One Time Toke -func (s *TokenServiceServer) CancelTokenByUID(ctx context.Context, in *permission.DoTokenByUIDReq) (*permission.OKResp, error) { - l := logic.NewCancelTokenByUIDLogic(ctx, s.svcCtx) - return l.CancelTokenByUID(in) -} - -// CancelTokenByDeviceID 取消 Token -func (s *TokenServiceServer) CancelTokenByDeviceID(ctx context.Context, in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) { - l := logic.NewCancelTokenByDeviceIDLogic(ctx, s.svcCtx) - return l.CancelTokenByDeviceID(in) -} - // ValidationToken 驗證這個 Token 有沒有效 func (s *TokenServiceServer) ValidationToken(ctx context.Context, in *permission.ValidationTokenReq) (*permission.ValidationTokenResp, error) { - l := logic.NewValidationTokenLogic(ctx, s.svcCtx) + l := tokenservicelogic.NewValidationTokenLogic(ctx, s.svcCtx) return l.ValidationToken(in) } -// GetUserTokensByDeviceIDs 取得目前所對應的 DeviceID 所存在的 Tokens -func (s *TokenServiceServer) GetUserTokensByDeviceID(ctx context.Context, in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) { - l := logic.NewGetUserTokensByDeviceIDLogic(ctx, s.svcCtx) - return l.GetUserTokensByDeviceID(in) +// CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出 +func (s *TokenServiceServer) CancelTokens(ctx context.Context, in *permission.DoTokenByUIDReq) (*permission.OKResp, error) { + l := tokenservicelogic.NewCancelTokensLogic(ctx, s.svcCtx) + return l.CancelTokens(in) } -// GetUserTokensByUID 取得目前所對應的 UID 所存在的 Tokens -func (s *TokenServiceServer) GetUserTokensByUID(ctx context.Context, in *permission.DoTokenByUIDReq) (*permission.Tokens, error) { - l := logic.NewGetUserTokensByUIDLogic(ctx, s.svcCtx) - return l.GetUserTokensByUID(in) +// CancelTokenByDeviceId 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token +func (s *TokenServiceServer) CancelTokenByDeviceId(ctx context.Context, in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) { + l := tokenservicelogic.NewCancelTokenByDeviceIdLogic(ctx, s.svcCtx) + return l.CancelTokenByDeviceId(in) +} + +// GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens +func (s *TokenServiceServer) GetUserTokensByDeviceId(ctx context.Context, in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) { + l := tokenservicelogic.NewGetUserTokensByDeviceIdLogic(ctx, s.svcCtx) + return l.GetUserTokensByDeviceId(in) +} + +// GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens +func (s *TokenServiceServer) GetUserTokensByUid(ctx context.Context, in *permission.QueryTokenByUIDReq) (*permission.Tokens, error) { + l := tokenservicelogic.NewGetUserTokensByUidLogic(ctx, s.svcCtx) + return l.GetUserTokensByUid(in) } // NewOneTimeToken 建立一次性使用,例如:RefreshToken func (s *TokenServiceServer) NewOneTimeToken(ctx context.Context, in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) { - l := logic.NewNewOneTimeTokenLogic(ctx, s.svcCtx) + l := tokenservicelogic.NewNewOneTimeTokenLogic(ctx, s.svcCtx) return l.NewOneTimeToken(in) } // CancelOneTimeToken 取消一次性使用 -func (s *TokenServiceServer) CancelOneTimeToken(ctx context.Context, in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) { - l := logic.NewCancelOneTimeTokenLogic(ctx, s.svcCtx) +func (s *TokenServiceServer) CancelOneTimeToken(ctx context.Context, in *permission.CancelOneTimeTokenReq) (*permission.OKResp, error) { + l := tokenservicelogic.NewCancelOneTimeTokenLogic(ctx, s.svcCtx) return l.CancelOneTimeToken(in) } diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 9541b6b..2784204 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -1,13 +1,36 @@ package svc -import "ark-permission/internal/config" +import ( + "ark-permission/internal/config" + "ark-permission/internal/domain/repository" + "ark-permission/internal/lib/required" + repo "ark-permission/internal/repository" + ers "code.30cm.net/wanderland/library-go/errors" + "code.30cm.net/wanderland/library-go/errors/code" + "github.com/zeromicro/go-zero/core/stores/redis" +) type ServiceContext struct { Config config.Config + + Validate required.Validate + Redis redis.Redis + TokenRedisRepo repository.TokenRepository } func NewServiceContext(c config.Config) *ServiceContext { + newRedis, err := redis.NewRedis(c.RedisCluster, redis.Cluster()) + if err != nil { + panic(err) + } + ers.Scope = code.CloudEPPermission + return &ServiceContext{ - Config: c, + Config: c, + Validate: required.MustValidator(), + Redis: *newRedis, + TokenRedisRepo: repo.NewTokenRepository(repo.TokenRepositoryParam{ + Store: newRedis, + }), } } diff --git a/permission.go b/permission.go index 7309554..9fe85f9 100644 --- a/permission.go +++ b/permission.go @@ -1,12 +1,14 @@ package main import ( + permissionservice "ark-permission/internal/server/permissionservice" + roleservice "ark-permission/internal/server/roleservice" + tokenservice "ark-permission/internal/server/tokenservice" "flag" "fmt" "ark-permission/gen_result/pb/permission" "ark-permission/internal/config" - "ark-permission/internal/server" "ark-permission/internal/svc" "github.com/zeromicro/go-zero/core/conf" @@ -26,7 +28,9 @@ func main() { ctx := svc.NewServiceContext(c) s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { - permission.RegisterTokenServiceServer(grpcServer, server.NewTokenServiceServer(ctx)) + permission.RegisterTokenServiceServer(grpcServer, tokenservice.NewTokenServiceServer(ctx)) + permission.RegisterRoleServiceServer(grpcServer, roleservice.NewRoleServiceServer(ctx)) + permission.RegisterPermissionServiceServer(grpcServer, permissionservice.NewPermissionServiceServer(ctx)) if c.Mode == service.DevMode || c.Mode == service.TestMode { reflection.Register(grpcServer) @@ -34,6 +38,9 @@ func main() { }) defer s.Stop() + // 加入中間件 + // s.AddUnaryInterceptors(middleware.TimeoutMiddleware) + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) s.Start() }