add token service #2
4
Makefile
4
Makefile
|
@ -70,5 +70,5 @@ mock-gen: # 建立 mock 資料
|
|||
|
||||
.PHONY: migrate-database
|
||||
migrate-database:
|
||||
migrate -source file://generate/database/mysql -database 'mysql://root:yytt@tcp(127.0.0.1:3306)/digimon_member' up
|
||||
migrate -source file://generate/database/seeders -database 'mysql://root:yytt@tcp(127.0.0.1:3306)/digimon_member' up
|
||||
migrate -source file://generate/database/mysql -database 'mysql://root:yytt@tcp(127.0.0.1:3306)/digimon_permission' up
|
||||
migrate -source file://generate/database/seeders -database 'mysql://root:yytt@tcp(127.0.0.1:3306)/digimon_permission' up
|
||||
|
|
13
go.mod
13
go.mod
|
@ -3,12 +3,18 @@ module app-cloudep-permission-server
|
|||
go 1.22.3
|
||||
|
||||
require (
|
||||
code.30cm.net/digimon/library-go/errors v1.0.1
|
||||
code.30cm.net/digimon/library-go/validator v1.0.0
|
||||
code.30cm.net/wanderland/library-go/errors v1.0.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.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
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
@ -18,11 +24,16 @@ 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/go-playground/validator/v10 v10.22.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.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
|
||||
|
@ -33,6 +44,7 @@ require (
|
|||
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 +77,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
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
package config
|
||||
|
||||
import "github.com/zeromicro/go-zero/zrpc"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"github.com/zeromicro/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
zrpc.RpcServerConf
|
||||
RedisCluster redis.RedisConf
|
||||
Token struct {
|
||||
RefreshExpires time.Duration
|
||||
Expired time.Duration
|
||||
Secret string
|
||||
}
|
||||
// 加上DB結構體
|
||||
DB struct {
|
||||
DsnString string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -0,0 +1,83 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
mts "app-cloudep-permission-server/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 以及分類要系統共用,係向的錯誤各自服務實作就好
|
||||
|
||||
// token error 方面
|
||||
const (
|
||||
TokenUnexpectedSigningErrorCode = iota + 1
|
||||
TokenValidateErrorCode
|
||||
TokenClaimErrorCode
|
||||
)
|
||||
|
||||
const (
|
||||
RedisDelErrorCode = iota + 20
|
||||
RedisPipLineErrorCode
|
||||
RedisErrorCode
|
||||
)
|
||||
|
||||
const (
|
||||
PermissionNotFoundCode = iota + 30
|
||||
PermissionGetDataErrorCode
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// PermissionNotFoundError 30030 權限錯誤
|
||||
func PermissionNotFoundError(msg string) *ers.Err {
|
||||
// 看需要建立哪些 Metrics
|
||||
return ers.NewErr(code.CloudEPPermission, code.Forbidden, PermissionNotFoundCode, msg)
|
||||
}
|
||||
|
||||
// PermissionGetDataError 30031 解析權限時錯誤
|
||||
func PermissionGetDataError(msg string) *ers.Err {
|
||||
// 看需要建立哪些 Metrics
|
||||
return ers.NewErr(code.CloudEPPermission, code.InvalidFormat, PermissionGetDataErrorCode, msg)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package domain
|
||||
|
||||
type PermissionType int8
|
||||
|
||||
const (
|
||||
PermissionTypeBackendUser PermissionType = iota + 1
|
||||
PermissionTypeFrontendUser
|
||||
)
|
||||
|
||||
type PermissionTypeCode string
|
||||
|
||||
const (
|
||||
PermissionTypeBackCode PermissionTypeCode = "back"
|
||||
PermissionTypeFrontCode PermissionTypeCode = "front"
|
||||
)
|
||||
|
||||
var permissionMap = map[int64]PermissionTypeCode{
|
||||
1: PermissionTypeFrontCode,
|
||||
2: PermissionTypeBackCode,
|
||||
}
|
||||
|
||||
func ToPermissionTypeCode(code int64) (PermissionTypeCode, bool) {
|
||||
result, ok := permissionMap[code]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return result, true
|
||||
}
|
||||
|
||||
func (t *PermissionTypeCode) ToString() string {
|
||||
return string(*t)
|
||||
}
|
||||
|
||||
type PermissionStatus string
|
||||
type Permissions map[string]PermissionStatus
|
||||
|
||||
const (
|
||||
PermissionStatusOpenCode PermissionStatus = "open"
|
||||
PermissionStatusCloseCode PermissionStatus = "close"
|
||||
)
|
||||
|
||||
const (
|
||||
AdminRoleID = "GodDog!@#"
|
||||
)
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
mts "app-cloudep-permission-server/internal/lib/metric"
|
||||
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"code.30cm.net/digimon/library-go/errors/code"
|
||||
)
|
||||
|
||||
// token error 方面
|
||||
const (
|
||||
TokenUnexpectedSigningErrorCode = iota + 1
|
||||
TokenValidateErrorCode
|
||||
TokenClaimErrorCode
|
||||
)
|
||||
|
||||
const (
|
||||
RedisDelErrorCode = iota + 20
|
||||
RedisPipLineErrorCode
|
||||
RedisErrorCode
|
||||
)
|
||||
|
||||
// TokenUnexpectedSigningErr 30001 Token 簽名錯誤
|
||||
func TokenUnexpectedSigningErr(msg string) *ers.LibError {
|
||||
mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign")
|
||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg)
|
||||
}
|
||||
|
||||
// TokenTokenValidateErr 30002 Token 驗證錯誤
|
||||
func TokenTokenValidateErr(msg string) *ers.LibError {
|
||||
mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal")
|
||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg)
|
||||
}
|
||||
|
||||
// TokenClaimError 30003 Token 驗證錯誤
|
||||
func TokenClaimError(msg string) *ers.LibError {
|
||||
mts.AppErrorMetrics.AddFailure("token", "token_claim_error")
|
||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg)
|
||||
}
|
||||
|
||||
// RedisDelError 30020 Redis 刪除錯誤
|
||||
func RedisDelError(msg string) *ers.LibError {
|
||||
// 看需要建立哪些 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.LibError {
|
||||
// 看需要建立哪些 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.LibError {
|
||||
// 看需要建立哪些 Metrics
|
||||
mts.AppErrorMetrics.AddFailure("redis", "error")
|
||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"app-cloudep-permission-server/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
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type OpaUseCase interface {
|
||||
// CheckRBACPermission 確認有無權限
|
||||
CheckRBACPermission(ctx context.Context, req CheckReq) (CheckOPAResp, error)
|
||||
// LoadPolicy 將 Policy 從其他地方加載到 opa 的 policy 當中
|
||||
LoadPolicy(ctx context.Context, input []Policy) error
|
||||
GetPolicy(ctx context.Context) []map[string]any
|
||||
}
|
||||
|
||||
type CheckReq struct {
|
||||
ID string
|
||||
Roles []string
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
type Grant struct {
|
||||
ID string
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
Methods []string `json:"methods"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type RuleRequest struct {
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Policies []Policy `json:"policies"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
type CheckOPAResp struct {
|
||||
Allow bool `json:"allow"`
|
||||
PolicyName string `json:"policy_name"`
|
||||
PlainCode bool `json:"plain_code"` // 是否為明碼顯示
|
||||
Request RuleRequest `json:"request"`
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"ark-permission/internal/domain"
|
||||
"ark-permission/internal/entity"
|
||||
)
|
||||
|
||||
// PermissionTreeManager 定義一組操作權限樹的接口
|
||||
// 這個名稱說明它是專門負責管理和操作權限樹的管理器
|
||||
type PermissionTreeManager interface {
|
||||
// AddPermission 將一個新的權限節點插入到樹中
|
||||
// key 是父節點的ID,value 是要插入的 Permission 資料
|
||||
// 此方法應該能處理節點是否存在於父節點下的情況
|
||||
AddPermission(parentID int64, permission entity.Permission) error
|
||||
// FindPermissionByID 根據權限 ID 查詢樹中的某個節點
|
||||
// 如果節點存在,返回對應的 Permission 資料,否則返回 nil
|
||||
FindPermissionByID(permissionID int64) (*Permission, error)
|
||||
// GetAllParentPermissionIDs 根據傳入的 permissions 列表
|
||||
// 找出每個權限的完整父節點權限 ID 路徑
|
||||
// 例如,如果 B 的父權限是 A,並且給了 B 權限,則返回 A 和 B 的權限 ID
|
||||
GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error)
|
||||
// GetAllParentPermissionStatuses 返回給定權限下的所有完整父節點權限狀態
|
||||
// 例如,若給 B 權限,該方法將返回所有與 B 相關的父權限的狀態
|
||||
GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error)
|
||||
// GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態
|
||||
// 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態
|
||||
GetRolePermissionTree(rolePermissions []entity.RolePermission) domain.Permissions
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
ID int64 `json:"-"`
|
||||
Name string `json:"name"`
|
||||
HTTPMethod string `json:"http_method"`
|
||||
HTTPPath string `json:"http_path"`
|
||||
Parent *Permission `json:"-"`
|
||||
Children []*Permission `json:"children"`
|
||||
PathIDs []int64 `json:"-"` // full path id
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package entity
|
||||
|
||||
import "github.com/golang-jwt/jwt/v4"
|
||||
|
||||
type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
Data interface{} `json:"data"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -3,6 +3,8 @@ package tokenservicelogic
|
|||
import (
|
||||
"context"
|
||||
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
"app-cloudep-permission-server/internal/svc"
|
||||
|
||||
|
@ -23,9 +25,23 @@ func NewCancelOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
|||
}
|
||||
}
|
||||
|
||||
type cancelOneTimeTokenReq struct {
|
||||
Token []string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
// CancelOneTimeToken 取消一次性使用
|
||||
func (l *CancelOneTimeTokenLogic) CancelOneTimeToken(in *permission.CancelOneTimeTokenReq) (*permission.OKResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
// 驗證所需
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
|
@ -25,7 +26,19 @@ func NewCancelTokenByDeviceIdLogic(ctx context.Context, svcCtx *svc.ServiceConte
|
|||
|
||||
// CancelTokenByDeviceId 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token
|
||||
func (l *CancelTokenByDeviceIdLogic) CancelTokenByDeviceId(in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
|
@ -23,9 +24,44 @@ func NewCancelTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Cance
|
|||
}
|
||||
}
|
||||
|
||||
type cancelTokenReq struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
// CancelToken 取消 Token,也包含他裡面的 One Time Toke
|
||||
func (l *CancelTokenLogic) CancelToken(in *permission.CancelTokenReq) (*permission.OKResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
// 驗證所需
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
|
@ -25,7 +26,27 @@ func NewCancelTokensLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Canc
|
|||
|
||||
// CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出
|
||||
func (l *CancelTokensLogic) CancelTokens(in *permission.DoTokenByUIDReq) (*permission.OKResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
"app-cloudep-permission-server/internal/domain"
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
|
@ -23,9 +25,34 @@ func NewGetUserTokensByDeviceIdLogic(ctx context.Context, svcCtx *svc.ServiceCon
|
|||
}
|
||||
}
|
||||
|
||||
type getUserTokensByDeviceIdReq struct {
|
||||
DeviceID string `json:"device_id" validate:"required"`
|
||||
}
|
||||
|
||||
// GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens
|
||||
func (l *GetUserTokensByDeviceIdLogic) GetUserTokensByDeviceId(in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
if err := l.svcCtx.Validate.ValidateAll(&getUserTokensByDeviceIdReq{
|
||||
DeviceID: in.GetDeviceId(),
|
||||
}); err != nil {
|
||||
return nil, ers.InvalidFormat(err.Error())
|
||||
}
|
||||
|
||||
return &permission.Tokens{}, nil
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
"app-cloudep-permission-server/internal/domain"
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
|
@ -23,9 +25,34 @@ func NewGetUserTokensByUidLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
|||
}
|
||||
}
|
||||
|
||||
type getUserTokensByUidReq struct {
|
||||
UID string `json:"uid" validate:"required"`
|
||||
}
|
||||
|
||||
// GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens
|
||||
func (l *GetUserTokensByUidLogic) GetUserTokensByUid(in *permission.QueryTokenByUIDReq) (*permission.Tokens, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
if err := l.svcCtx.Validate.ValidateAll(&getUserTokensByUidReq{
|
||||
UID: in.GetUid(),
|
||||
}); err != nil {
|
||||
return nil, ers.InvalidFormat(err.Error())
|
||||
}
|
||||
|
||||
return &permission.Tokens{}, nil
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
"app-cloudep-permission-server/internal/domain"
|
||||
"app-cloudep-permission-server/internal/entity"
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
"app-cloudep-permission-server/internal/svc"
|
||||
|
@ -25,7 +30,41 @@ func NewNewOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *N
|
|||
|
||||
// NewOneTimeToken 建立一次性使用,例如:RefreshToken
|
||||
func (l *NewOneTimeTokenLogic) NewOneTimeToken(in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
// 驗證所需
|
||||
if err := l.svcCtx.Validate.ValidateAll(&refreshTokenReq{
|
||||
Token: in.GetToken(),
|
||||
}); err != nil {
|
||||
return nil, ers.InvalidFormat(err.Error())
|
||||
}
|
||||
|
||||
return &permission.CreateOneTimeTokenResp{}, nil
|
||||
// 驗證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
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
"app-cloudep-permission-server/internal/config"
|
||||
"app-cloudep-permission-server/internal/domain"
|
||||
"app-cloudep-permission-server/internal/entity"
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
"app-cloudep-permission-server/internal/svc"
|
||||
|
@ -23,9 +29,110 @@ func NewNewTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewToken
|
|||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// todo: add your logic here and delete this line
|
||||
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
|
||||
}
|
||||
|
||||
return &permission.TokenResp{}, nil
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
"app-cloudep-permission-server/internal/domain"
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
|
@ -23,9 +25,83 @@ func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Refr
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// todo: add your logic here and delete this line
|
||||
// 驗證所需
|
||||
if err := l.svcCtx.Validate.ValidateAll(&refreshReq{
|
||||
RefreshToken: in.GetToken(),
|
||||
Scope: in.GetScope(),
|
||||
DeviceID: in.GetDeviceId(),
|
||||
}); err != nil {
|
||||
return nil, ers.InvalidFormat(err.Error())
|
||||
}
|
||||
|
||||
return &permission.RefreshTokenResp{}, nil
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
"app-cloudep-permission-server/internal/domain"
|
||||
"app-cloudep-permission-server/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
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package tokenservicelogic
|
||||
|
||||
import (
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"context"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
|
@ -23,9 +24,46 @@ func NewValidationTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *V
|
|||
}
|
||||
}
|
||||
|
||||
type refreshTokenReq struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
// ValidationToken 驗證這個 Token 有沒有效
|
||||
func (l *ValidationTokenLogic) ValidationToken(in *permission.ValidationTokenReq) (*permission.ValidationTokenResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
// 驗證所需
|
||||
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{}, nil
|
||||
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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,302 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"app-cloudep-permission-server/internal/domain"
|
||||
"app-cloudep-permission-server/internal/domain/repository"
|
||||
"app-cloudep-permission-server/internal/entity"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
|
||||
"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 repository.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 repository.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 repository.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{}, repository.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 repository.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{}, repository.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 repository.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, repository.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
|
||||
}
|
|
@ -7,7 +7,7 @@ import (
|
|||
"context"
|
||||
|
||||
"app-cloudep-permission-server/gen_result/pb/permission"
|
||||
"app-cloudep-permission-server/internal/logic/tokenservice"
|
||||
tokenservicelogic "app-cloudep-permission-server/internal/logic/tokenservice"
|
||||
"app-cloudep-permission-server/internal/svc"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,13 +1,42 @@
|
|||
package svc
|
||||
|
||||
import "app-cloudep-permission-server/internal/config"
|
||||
import (
|
||||
"app-cloudep-permission-server/internal/config"
|
||||
"app-cloudep-permission-server/internal/domain/repository"
|
||||
repo "app-cloudep-permission-server/internal/repository"
|
||||
|
||||
ers "code.30cm.net/digimon/library-go/errors"
|
||||
"code.30cm.net/digimon/library-go/errors/code"
|
||||
vi "code.30cm.net/digimon/library-go/validator"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
type ServiceContext struct {
|
||||
Config config.Config
|
||||
Conn sqlx.SqlConn
|
||||
|
||||
Validate vi.Validate
|
||||
Redis redis.Redis
|
||||
TokenRedisRepo repository.TokenRepository
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
ers.Scope = code.CloudEPPermission
|
||||
sqlConn := sqlx.NewMysql(c.DB.DsnString)
|
||||
|
||||
newRedis, err := redis.NewRedis(c.RedisCluster, redis.Cluster())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Conn: sqlConn,
|
||||
Config: c,
|
||||
Validate: vi.MustValidator(),
|
||||
Redis: *newRedis,
|
||||
TokenRedisRepo: repo.NewTokenRepository(repo.TokenRepositoryParam{
|
||||
Store: newRedis,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue