diff --git a/devcloud/.vscode/settings.json b/devcloud/.vscode/settings.json new file mode 100644 index 0000000..117fb4b --- /dev/null +++ b/devcloud/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "go.testEnvVars": { + "workspaceFolder": "${workspaceFolder}", + "CONFIG_PATH": "${workspaceFolder}/etc/application.toml" + }, + "go.testEnvFile": "${workspaceFolder}/etc/unit_test.env" +} \ No newline at end of file diff --git a/devcloud/etc/unit_test.env b/devcloud/etc/unit_test.env new file mode 100644 index 0000000..e69de29 diff --git a/devcloud/mcenter/apps/registry.go b/devcloud/mcenter/apps/registry.go index cff2ab9..315c7cc 100644 --- a/devcloud/mcenter/apps/registry.go +++ b/devcloud/mcenter/apps/registry.go @@ -1 +1,12 @@ package apps + +import ( + _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user/api" + _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user/impl" + + _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/api" + _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/impl" + + // 颁发器 + _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers" +) diff --git a/devcloud/mcenter/apps/token/api/api.go b/devcloud/mcenter/apps/token/api/api.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/devcloud/mcenter/apps/token/api/api.go @@ -0,0 +1 @@ +package api diff --git a/devcloud/mcenter/apps/token/api/token.go b/devcloud/mcenter/apps/token/api/token.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/devcloud/mcenter/apps/token/api/token.go @@ -0,0 +1 @@ +package api diff --git a/devcloud/mcenter/apps/token/const.go b/devcloud/mcenter/apps/token/const.go new file mode 100644 index 0000000..d3c7d5d --- /dev/null +++ b/devcloud/mcenter/apps/token/const.go @@ -0,0 +1,21 @@ +package token + +import "github.com/infraboard/mcube/v2/exception" + +const ( + ACCESS_TOKEN_HEADER_NAME = "Authorization" + ACCESS_TOKEN_COOKIE_NAME = "access_token" + ACCESS_TOKEN_RESPONSE_HEADER_NAME = "X-OAUTH-TOKEN" + REFRESH_TOKEN_HEADER_NAME = "X-REFRUSH-TOKEN" +) + +// 自定义非导出类型,避免外部包直接实例化 +type tokenContextKey struct{} + +var ( + CTX_TOKEN_KEY = tokenContextKey{} +) + +var ( + CookieNotFound = exception.NewUnauthorized("cookie %s not found", ACCESS_TOKEN_COOKIE_NAME) +) diff --git a/devcloud/mcenter/apps/token/enum.go b/devcloud/mcenter/apps/token/enum.go index dd2ce47..312dfe2 100644 --- a/devcloud/mcenter/apps/token/enum.go +++ b/devcloud/mcenter/apps/token/enum.go @@ -29,3 +29,9 @@ const ( // 异常Ip登陆 LOCK_TYPE_OTHER_IP_LOGGED_IN ) + +type DESCRIBE_BY int + +const ( + DESCRIBE_BY_ACCESS_TOKEN DESCRIBE_BY = iota +) diff --git a/devcloud/mcenter/apps/token/impl/impl.go b/devcloud/mcenter/apps/token/impl/impl.go index 4f9d22e..dc2a608 100644 --- a/devcloud/mcenter/apps/token/impl/impl.go +++ b/devcloud/mcenter/apps/token/impl/impl.go @@ -1 +1,56 @@ package impl + +import ( + "time" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user" + "github.com/infraboard/mcube/v2/ioc" + "github.com/infraboard/mcube/v2/ioc/config/datasource" + "github.com/infraboard/mcube/v2/ioc/config/log" + "github.com/rs/zerolog" +) + +func init() { + ioc.Controller().Registry(&TokenServiceImpl{ + AutoRefresh: true, + RereshTTLSecond: 1 * 60 * 60, + }) +} + +var _ token.Service = (*TokenServiceImpl)(nil) + +type TokenServiceImpl struct { + ioc.ObjectImpl + user user.Service + log *zerolog.Logger + // policy policy.PermissionService + + // 自动刷新, 直接刷新Token的过期时间,而不是生成一个新Token + AutoRefresh bool `json:"auto_refresh" toml:"auto_refresh" yaml:"auto_refresh" env:"AUTO_REFRESH"` + // 刷新TTL + RereshTTLSecond uint64 `json:"refresh_ttl" toml:"refresh_ttl" yaml:"refresh_ttl" env:"REFRESH_TTL"` + // Api最多多少个, 这种Token往往过期时间比较长, 为了安全不要申请太多 + MaxActiveApiToken uint8 `json:"max_active_api_token" toml:"max_active_api_token" yaml:"max_active_api_token" env:"MAX_ACTIVE_API_TOKEN"` + + refreshDuration time.Duration +} + +func (i *TokenServiceImpl) Init() error { + i.log = log.Sub(i.Name()) + i.user = user.GetService() + // i.policy = policy.GetService() + i.refreshDuration = time.Duration(i.RereshTTLSecond) * time.Second + + if datasource.Get().AutoMigrate { + err := datasource.DB().AutoMigrate(&token.Token{}) + if err != nil { + return err + } + } + return nil +} + +func (i *TokenServiceImpl) Name() string { + return token.APP_NAME +} diff --git a/devcloud/mcenter/apps/token/impl/impl_test.go b/devcloud/mcenter/apps/token/impl/impl_test.go new file mode 100644 index 0000000..4a96a50 --- /dev/null +++ b/devcloud/mcenter/apps/token/impl/impl_test.go @@ -0,0 +1,18 @@ +package impl_test + +import ( + "context" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" + "122.51.31.227/go-course/go18/devcloud/mcenter/test" +) + +var ( + svc token.Service + ctx = context.Background() +) + +func init() { + test.DevelopmentSet() + svc = token.GetService() +} diff --git a/devcloud/mcenter/apps/token/impl/token.go b/devcloud/mcenter/apps/token/impl/token.go index 4f9d22e..da95ffa 100644 --- a/devcloud/mcenter/apps/token/impl/token.go +++ b/devcloud/mcenter/apps/token/impl/token.go @@ -1 +1,201 @@ package impl + +import ( + "context" + "time" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" + "github.com/infraboard/mcube/v2/desense" + "github.com/infraboard/mcube/v2/exception" + "github.com/infraboard/mcube/v2/ioc/config/datasource" + "github.com/infraboard/mcube/v2/types" +) + +// 登录接口(颁发Token) +func (i *TokenServiceImpl) IssueToken(ctx context.Context, in *token.IssueTokenRequest) (*token.Token, error) { + // 颁发Token + // user/password + // ldap + // 飞书,企业微信 ... + issuer := token.GetIssuer(in.Issuer) + if issuer == nil { + return nil, exception.NewBadRequest("issuer %s no support", in.Issuer) + } + tk, err := issuer.IssueToken(ctx, in.Parameter) + if err != nil { + return nil, err + } + tk.SetIssuer(in.Issuer).SetSource(in.Source) + + // 判断当前数据库有没有已经存在的Token + activeTokenQueryReq := token.NewQueryTokenRequest(). + AddUserId(tk.UserId). + SetSource(in.Source). + SetActive(true) + tks, err := i.QueryToken(ctx, activeTokenQueryReq) + if err != nil { + return nil, err + } + switch in.Source { + // 每个端只能有1个活跃登录 + case token.SOURCE_WEB, token.SOURCE_IOS, token.SOURCE_ANDROID, token.SOURCE_PC: + if tks.Len() > 0 { + i.log.Debug().Msgf("use exist active token: %s", desense.Default().DeSense(tk.AccessToken, "4", "3")) + return tks.Items[0], nil + } + case token.SOURCE_API: + if tks.Len() > int(i.MaxActiveApiToken) { + return nil, exception.NewBadRequest("max active api token overflow") + } + } + + // 保持Token + if err := datasource.DBFromCtx(ctx). + Create(tk). + Error; err != nil { + return nil, err + } + + return tk, nil +} + +// 校验Token 是给内部中间层使用 身份校验层 +func (i *TokenServiceImpl) ValiateToken(ctx context.Context, req *token.ValiateTokenRequest) (*token.Token, error) { + // 1. 查询Token (是不是我们这个系统颁发的) + tk := token.NewToken() + err := datasource.DBFromCtx(ctx). + Where("access_token = ?", req.AccessToken). + First(tk). + Error + if err != nil { + return nil, err + } + + // 2.1 判断Ak是否过期 + if err := tk.IsAccessTokenExpired(); err != nil { + // 判断刷新Token是否过期 + if err := tk.IsRreshTokenExpired(); err != nil { + return nil, err + } + + // 如果开启了自动刷新 + if i.AutoRefresh { + tk.SetRefreshAt(time.Now()) + tk.SetExpiredAtByDuration(i.refreshDuration, 4) + if err := datasource.DBFromCtx(ctx).Save(tk); err != nil { + i.log.Error().Msgf("auto refresh token error, %s", err.Error) + } + } + + return nil, err + } + + return tk, nil +} + +func (i *TokenServiceImpl) DescribeToken(ctx context.Context, in *token.DescribeTokenRequest) (*token.Token, error) { + query := datasource.DBFromCtx(ctx) + switch in.DescribeBy { + case token.DESCRIBE_BY_ACCESS_TOKEN: + query = query.Where("access_token = ?", in.DescribeValue) + default: + return nil, exception.NewBadRequest("unspport describe type %s", in.DescribeValue) + } + + tk := token.NewToken() + if err := query.First(tk).Error; err != nil { + return nil, err + } + return tk, nil +} + +// 退出接口(销毁Token) +func (i *TokenServiceImpl) RevolkToken(ctx context.Context, in *token.RevolkTokenRequest) (*token.Token, error) { + tk, err := i.DescribeToken(ctx, token.NewDescribeTokenRequest(in.AccessToken)) + if err != nil { + return nil, err + } + if err := tk.CheckRefreshToken(in.RefreshToken); err != nil { + return nil, err + } + + tk.Lock(token.LOCK_TYPE_REVOLK, "user revolk token") + err = datasource.DBFromCtx(ctx).Model(&token.Token{}). + Where("access_token = ?", in.AccessToken). + Where("refresh_token = ?", in.RefreshToken). + Updates(tk.Status.ToMap()). + Error + if err != nil { + return nil, err + } + return tk, err +} + +// 查询已经颁发出去的Token +func (i *TokenServiceImpl) QueryToken(ctx context.Context, in *token.QueryTokenRequest) (*types.Set[*token.Token], error) { + set := types.New[*token.Token]() + query := datasource.DBFromCtx(ctx).Model(&token.Token{}) + + if in.Active != nil { + if *in.Active { + query = query. + Where("lock_at IS NULL AND refresh_token_expired_at > ?", time.Now()) + } else { + query = query. + Where("lock_at IS NOT NULL OR refresh_token_expired_at <= ?", time.Now()) + } + } + if in.Source != nil { + query = query.Where("source = ?", *in.Source) + } + if len(in.UserIds) > 0 { + query = query.Where("user_id IN ?", in.UserIds) + } + + // 查询总量 + err := query.Count(&set.Total).Error + if err != nil { + return nil, err + } + + err = query. + Order("issue_at desc"). + Offset(int(in.ComputeOffset())). + Limit(int(in.PageSize)). + Find(&set.Items). + Error + if err != nil { + return nil, err + } + + return set, nil +} + +// 用户切换空间 +// func (i *TokenServiceImpl) ChangeNamespce(ctx context.Context, in *token.ChangeNamespceRequest) (*token.Token, error) { +// set, err := i.policy.QueryNamespace(ctx, policy.NewQueryNamespaceRequest().SetUserId(in.UserId).SetNamespaceId(in.NamespaceId)) +// if err != nil { +// return nil, err +// } + +// ns := set.First() +// if ns == nil { +// return nil, exception.NewPermissionDeny("你没有该空间访问权限") +// } + +// // 更新Token +// tk, err := i.DescribeToken(ctx, token.NewDescribeTokenRequest(in.AccessToken)) +// if err != nil { +// return nil, err +// } +// tk.NamespaceId = ns.Id +// tk.NamespaceName = ns.Name + +// // 保存状态 +// if err := datasource.DBFromCtx(ctx). +// Updates(tk). +// Error; err != nil { +// return nil, err +// } +// return tk, nil +// } diff --git a/devcloud/mcenter/apps/token/impl/token_test.go b/devcloud/mcenter/apps/token/impl/token_test.go new file mode 100644 index 0000000..5e844e1 --- /dev/null +++ b/devcloud/mcenter/apps/token/impl/token_test.go @@ -0,0 +1,27 @@ +package impl_test + +import ( + "testing" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" +) + +func TestIssueToken(t *testing.T) { + req := token.NewIssueTokenRequest() + req.IssueByPassword("admin", "123456") + req.Source = token.SOURCE_WEB + set, err := svc.IssueToken(ctx, req) + if err != nil { + t.Fatal(err) + } + t.Log(set) +} + +func TestQueryToken(t *testing.T) { + req := token.NewQueryTokenRequest() + set, err := svc.QueryToken(ctx, req) + if err != nil { + t.Fatal(err) + } + t.Log(set) +} diff --git a/devcloud/mcenter/apps/token/interfaceg.go b/devcloud/mcenter/apps/token/interfaceg.go index 8485347..72fe4e4 100644 --- a/devcloud/mcenter/apps/token/interfaceg.go +++ b/devcloud/mcenter/apps/token/interfaceg.go @@ -3,18 +3,84 @@ package token import ( "context" "time" + + "github.com/infraboard/mcube/v2/http/request" + "github.com/infraboard/mcube/v2/ioc" + "github.com/infraboard/mcube/v2/types" ) +const ( + APP_NAME = "token" +) + +func GetService() Service { + return ioc.Controller().Get(APP_NAME).(Service) +} + type Service interface { // 颁发访问令牌: Login IssueToken(context.Context, *IssueTokenRequest) (*Token, error) // 撤销访问令牌: 令牌失效了 Logout RevolkToken(context.Context, *RevolkTokenRequest) (*Token, error) + // 查询已经颁发出去的Token + QueryToken(context.Context, *QueryTokenRequest) (*types.Set[*Token], error) + // 查询Token详情 + DescribeToken(context.Context, *DescribeTokenRequest) (*Token, error) // 校验访问令牌:检查令牌的合法性, 是不是伪造的 ValiateToken(context.Context, *ValiateTokenRequest) (*Token, error) } +func NewDescribeTokenRequest(accessToken string) *DescribeTokenRequest { + return &DescribeTokenRequest{ + DescribeBy: DESCRIBE_BY_ACCESS_TOKEN, + DescribeValue: accessToken, + } +} + +type DescribeTokenRequest struct { + DescribeBy DESCRIBE_BY `json:"describe_by"` + DescribeValue string `json:"describe_value"` +} + +func NewQueryTokenRequest() *QueryTokenRequest { + return &QueryTokenRequest{ + PageRequest: request.NewDefaultPageRequest(), + UserIds: []uint64{}, + } +} + +type QueryTokenRequest struct { + *request.PageRequest + // 当前可用的没过期的Token + Active *bool `json:"active"` + // 用户来源 + Source *SOURCE `json:"source"` + // Uids + UserIds []uint64 `json:"user_ids"` +} + +func (r *QueryTokenRequest) SetActive(v bool) *QueryTokenRequest { + r.Active = &v + return r +} + +func (r *QueryTokenRequest) SetSource(v SOURCE) *QueryTokenRequest { + r.Source = &v + return r +} + +func (r *QueryTokenRequest) AddUserId(uids ...uint64) *QueryTokenRequest { + r.UserIds = append(r.UserIds, uids...) + return r +} + +func NewIssueTokenRequest() *IssueTokenRequest { + return &IssueTokenRequest{ + Parameter: make(IssueParameter), + } +} + // 用户会给我们 用户的身份凭证,用于换取Token type IssueTokenRequest struct { // 端类型 @@ -25,6 +91,16 @@ type IssueTokenRequest struct { Parameter IssueParameter `json:"parameter"` } +func (i *IssueTokenRequest) IssueByPassword(username, password string) { + i.Issuer = ISSUER_PASSWORD + i.Parameter.SetUsername(username) + i.Parameter.SetPassword(password) +} + +func NewIssueParameter() IssueParameter { + return make(IssueParameter) +} + type IssueParameter map[string]any /* @@ -39,12 +115,14 @@ func (p IssueParameter) Password() string { return GetIssueParameterValue[string](p, "password") } -func (p IssueParameter) SetUsername(v string) { +func (p IssueParameter) SetUsername(v string) IssueParameter { p["username"] = v + return p } -func (p IssueParameter) SetPassword(v string) { +func (p IssueParameter) SetPassword(v string) IssueParameter { p["password"] = v + return p } /* @@ -59,8 +137,14 @@ func (p IssueParameter) ExpireTTL() time.Duration { return time.Second * time.Duration(GetIssueParameterValue[int64](p, "expired_ttl")) } -func (p IssueParameter) SetAccessToken(v string) { +func (p IssueParameter) SetAccessToken(v string) IssueParameter { p["access_token"] = v + return p +} + +func (p IssueParameter) SetExpireTTL(v int64) IssueParameter { + p["expired_ttl"] = v + return p } func NewRevolkTokenRequest(at, rk string) *RevolkTokenRequest { diff --git a/devcloud/mcenter/apps/token/issuer.go b/devcloud/mcenter/apps/token/issuer.go new file mode 100644 index 0000000..d1cb336 --- /dev/null +++ b/devcloud/mcenter/apps/token/issuer.go @@ -0,0 +1,44 @@ +package token + +import ( + "context" + "fmt" + "math/rand/v2" +) + +const ( + ISSUER_LDAP = "ldap" + ISSUER_FEISHU = "feishu" + ISSUER_PASSWORD = "password" + ISSUER_PRIVATE_TOKEN = "private_token" +) + +var issuers = map[string]Issuer{} + +func RegistryIssuer(name string, p Issuer) { + issuers[name] = p +} + +func GetIssuer(name string) Issuer { + fmt.Println(issuers) + return issuers[name] +} + +type Issuer interface { + IssueToken(context.Context, IssueParameter) (*Token, error) +} + +var ( + charlist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +) + +// MakeBearer https://tools.ietf.org/html/rfc6750#section-2.1 +// b64token = 1*( ALPHA / DIGIT /"-" / "." / "_" / "~" / "+" / "/" ) *"=" +func MakeBearer(lenth int) string { + t := make([]byte, 0) + for range lenth { + rn := rand.IntN(len(charlist)) + t = append(t, charlist[rn]) + } + return string(t) +} diff --git a/devcloud/mcenter/apps/token/issuer_test.go b/devcloud/mcenter/apps/token/issuer_test.go new file mode 100644 index 0000000..4324c1c --- /dev/null +++ b/devcloud/mcenter/apps/token/issuer_test.go @@ -0,0 +1,12 @@ +package token_test + +import ( + "testing" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" +) + +func TestMakeBearer(t *testing.T) { + t.Log(token.MakeBearer(24)) + t.Log(token.MakeBearer(24)) +} diff --git a/devcloud/mcenter/apps/token/issuers/password/issuer.go b/devcloud/mcenter/apps/token/issuers/password/issuer.go new file mode 100644 index 0000000..111b7f2 --- /dev/null +++ b/devcloud/mcenter/apps/token/issuers/password/issuer.go @@ -0,0 +1,67 @@ +package password + +import ( + "context" + "time" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user" + "github.com/infraboard/mcube/v2/exception" + "github.com/infraboard/mcube/v2/ioc" +) + +func init() { + ioc.Config().Registry(&PasswordTokenIssuer{ + ExpiredTTLSecond: 1 * 60 * 60, + }) +} + +type PasswordTokenIssuer struct { + ioc.ObjectImpl + // 通过用户模块 来判断用户凭证是否正确 + user user.Service + + // Password颁发的Token 过去时间由系统配置, 不允许用户自己设置 + ExpiredTTLSecond int `json:"expired_ttl_second" toml:"expired_ttl_second" yaml:"expired_ttl_second" env:"EXPIRED_TTL_SECOND"` + + expiredDuration time.Duration +} + +func (p *PasswordTokenIssuer) Name() string { + return "password_token_issuer" +} + +func (p *PasswordTokenIssuer) Init() error { + p.user = user.GetService() + p.expiredDuration = time.Duration(p.ExpiredTTLSecond) * time.Second + + token.RegistryIssuer(token.ISSUER_PASSWORD, p) + return nil +} + +func (p *PasswordTokenIssuer) IssueToken(ctx context.Context, parameter token.IssueParameter) (*token.Token, error) { + // 1. 查询用户 + uReq := user.NewDescribeUserRequestByUserName(parameter.Username()) + u, err := p.user.DescribeUser(ctx, uReq) + if err != nil { + if exception.IsNotFoundError(err) { + return nil, exception.NewUnauthorized("%s", err) + } + return nil, err + } + + // 2. 比对密码 + err = u.CheckPassword(parameter.Password()) + if err != nil { + return nil, err + } + + // 3. 颁发token + tk := token.NewToken() + tk.UserId = u.Id + tk.UserName = u.UserName + tk.IsAdmin = u.IsAdmin + + tk.SetExpiredAtByDuration(p.expiredDuration, 4) + return tk, nil +} diff --git a/devcloud/mcenter/apps/token/issuers/password/issuer_test.go b/devcloud/mcenter/apps/token/issuers/password/issuer_test.go new file mode 100644 index 0000000..b1b5555 --- /dev/null +++ b/devcloud/mcenter/apps/token/issuers/password/issuer_test.go @@ -0,0 +1,22 @@ +package password_test + +import ( + "context" + "testing" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" + "122.51.31.227/go-course/go18/devcloud/mcenter/test" +) + +func TestPasswordIssuer(t *testing.T) { + issuer := token.GetIssuer(token.ISSUER_PASSWORD) + tk, err := issuer.IssueToken(context.Background(), token.NewIssueParameter().SetUsername("admin").SetPassword("123456")) + if err != nil { + t.Fatal(err) + } + t.Log(tk) +} + +func init() { + test.DevelopmentSet() +} diff --git a/devcloud/mcenter/apps/token/issuers/private_token/issueer_test.go b/devcloud/mcenter/apps/token/issuers/private_token/issueer_test.go new file mode 100644 index 0000000..6a64dd7 --- /dev/null +++ b/devcloud/mcenter/apps/token/issuers/private_token/issueer_test.go @@ -0,0 +1,22 @@ +package privatetoken_test + +import ( + "context" + "testing" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" + "122.51.31.227/go-course/go18/devcloud/mcenter/test" +) + +func TestPasswordIssuer(t *testing.T) { + issuer := token.GetIssuer(token.ISSUER_PRIVATE_TOKEN) + tk, err := issuer.IssueToken(context.Background(), token.NewIssueParameter().SetAccessToken("LccvuTwISJRheu8PtqAFTJBy").SetExpireTTL(24*60*60)) + if err != nil { + t.Fatal(err) + } + t.Log(tk) +} + +func init() { + test.DevelopmentSet() +} diff --git a/devcloud/mcenter/apps/token/issuers/private_token/issuer.go b/devcloud/mcenter/apps/token/issuers/private_token/issuer.go new file mode 100644 index 0000000..d01a4fc --- /dev/null +++ b/devcloud/mcenter/apps/token/issuers/private_token/issuer.go @@ -0,0 +1,67 @@ +package privatetoken + +import ( + "context" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user" + "github.com/infraboard/mcube/v2/exception" + "github.com/infraboard/mcube/v2/ioc" +) + +func init() { + ioc.Config().Registry(&PrivateTokenIssuer{}) +} + +type PrivateTokenIssuer struct { + ioc.ObjectImpl + + user user.Service + token token.Service +} + +func (p *PrivateTokenIssuer) Name() string { + return "private_token_issuer" +} + +func (p *PrivateTokenIssuer) Init() error { + p.user = user.GetService() + p.token = token.GetService() + + token.RegistryIssuer(token.ISSUER_PRIVATE_TOKEN, p) + return nil +} + +func (p *PrivateTokenIssuer) IssueToken(ctx context.Context, parameter token.IssueParameter) (*token.Token, error) { + // 1. 校验Token合法 + oldTk, err := p.token.ValiateToken(ctx, token.NewValiateTokenRequest(parameter.AccessToken())) + if err != nil { + return nil, err + } + + // 2. 查询用户 + uReq := user.NewDescribeUserRequestById(oldTk.UserIdString()) + u, err := p.user.DescribeUser(ctx, uReq) + if err != nil { + if exception.IsNotFoundError(err) { + return nil, exception.NewUnauthorized("%s", err) + } + return nil, err + } + + if !u.EnabledApi { + return nil, exception.NewPermissionDeny("未开启接口登录") + } + + // 3. 颁发token + tk := token.NewToken() + tk.UserId = u.Id + tk.UserName = u.UserName + tk.IsAdmin = u.IsAdmin + + expiredTTL := parameter.ExpireTTL() + if expiredTTL > 0 { + tk.SetExpiredAtByDuration(expiredTTL, 4) + } + return tk, nil +} diff --git a/devcloud/mcenter/apps/token/issuers/registry.go b/devcloud/mcenter/apps/token/issuers/registry.go new file mode 100644 index 0000000..8481321 --- /dev/null +++ b/devcloud/mcenter/apps/token/issuers/registry.go @@ -0,0 +1,6 @@ +package issuers + +import ( + _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers/password" + _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers/private_token" +) diff --git a/devcloud/mcenter/apps/token/model.go b/devcloud/mcenter/apps/token/model.go index eb3a805..ccef9f5 100644 --- a/devcloud/mcenter/apps/token/model.go +++ b/devcloud/mcenter/apps/token/model.go @@ -1,13 +1,65 @@ package token import ( + "context" "fmt" + "net/http" + "net/url" + "strings" "time" "github.com/infraboard/mcube/v2/exception" "github.com/infraboard/mcube/v2/tools/pretty" ) +func GetAccessTokenFromHTTP(r *http.Request) string { + // 先从Token中获取 + tk := r.Header.Get(ACCESS_TOKEN_HEADER_NAME) + + // 1. 获取Token + if tk == "" { + cookie, err := r.Cookie(ACCESS_TOKEN_COOKIE_NAME) + if err != nil { + return "" + } + tk, _ = url.QueryUnescape(cookie.Value) + } else { + // 处理 带格式: Bearer + ft := strings.Split(tk, " ") + if len(ft) > 1 { + tk = ft[1] + } + } + return tk +} + +func GetTokenFromCtx(ctx context.Context) *Token { + if v := ctx.Value(CTX_TOKEN_KEY); v != nil { + return v.(*Token) + } + return nil +} + +func GetRefreshTokenFromHTTP(r *http.Request) string { + // 先从Token中获取 + tk := r.Header.Get(REFRESH_TOKEN_HEADER_NAME) + return tk +} + +func NewToken() *Token { + tk := &Token{ + // 生产一个UUID的字符串 + AccessToken: MakeBearer(24), + RefreshToken: MakeBearer(32), + IssueAt: time.Now(), + Status: NewStatus(), + Extras: map[string]string{}, + Scope: map[string]string{}, + } + + return tk +} + // 需要存储到数据库里面的对象(表) type Token struct { diff --git a/devcloud/mcenter/apps/user/README.md b/devcloud/mcenter/apps/user/README.md index 7813db2..a9ca452 100644 --- a/devcloud/mcenter/apps/user/README.md +++ b/devcloud/mcenter/apps/user/README.md @@ -43,4 +43,25 @@ password + 随机字符串(salt) --> (salt)hash它也是随机 输入: password + 随机字符串(salt: 原来hash中的sal部分) == hash它也是随机(数据库) +```go +import ( + "golang.org/x/crypto/bcrypt" +) +func (req *CreateUserRequest) PasswordHash() { + if req.isHashed { + return + } + + b, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + req.Password = string(b) + req.isHashed = true +} + +// 判断该用户的密码是否正确 +func (u *User) CheckPassword(password string) error { + // u.Password hash过后的只 + // (password 原始值 + hash值中提区salt) + return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) +} +``` diff --git a/devcloud/mcenter/apps/user/api/api.go b/devcloud/mcenter/apps/user/api/api.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/devcloud/mcenter/apps/user/api/api.go @@ -0,0 +1 @@ +package api diff --git a/devcloud/mcenter/apps/user/impl/impl_test.go b/devcloud/mcenter/apps/user/impl/impl_test.go index ed680d2..8fde448 100644 --- a/devcloud/mcenter/apps/user/impl/impl_test.go +++ b/devcloud/mcenter/apps/user/impl/impl_test.go @@ -1 +1,18 @@ package impl_test + +import ( + "context" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user" + "122.51.31.227/go-course/go18/devcloud/mcenter/test" +) + +var ( + impl user.Service + ctx = context.Background() +) + +func init() { + test.DevelopmentSet() + impl = user.GetService() +} diff --git a/devcloud/mcenter/apps/user/impl/user_test.go b/devcloud/mcenter/apps/user/impl/user_test.go index ed680d2..8cdb266 100644 --- a/devcloud/mcenter/apps/user/impl/user_test.go +++ b/devcloud/mcenter/apps/user/impl/user_test.go @@ -1 +1,91 @@ package impl_test + +import ( + "testing" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user" +) + +func TestQueryUser(t *testing.T) { + req := user.NewQueryUserRequest() + set, err := impl.QueryUser(ctx, req) + if err != nil { + t.Fatal(err) + } + t.Log(set) +} + +func TestCreateAdminUser(t *testing.T) { + req := user.NewCreateUserRequest() + req.UserName = "admin" + req.Password = "123456" + req.EnabledApi = true + req.IsAdmin = true + u, err := impl.CreateUser(ctx, req) + if err != nil { + t.Fatal(err) + } + t.Log(u) +} + +func TestCreateAuthor2(t *testing.T) { + req := user.NewCreateUserRequest() + req.UserName = "张三" + req.Password = "123456" + req.EnabledApi = true + u, err := impl.CreateUser(ctx, req) + if err != nil { + t.Fatal(err) + } + t.Log(u) +} + +func TestCreateGuestUser(t *testing.T) { + req := user.NewCreateUserRequest() + req.UserName = "guest" + req.Password = "123456" + req.EnabledApi = true + u, err := impl.CreateUser(ctx, req) + if err != nil { + t.Fatal(err) + } + t.Log(u) +} + +func TestDeleteUser(t *testing.T) { + _, err := impl.DeleteUser(ctx, &user.DeleteUserRequest{ + Id: "9", + }) + if err != nil { + t.Fatal(err) + } +} + +func TestDescribeUserRequestById(t *testing.T) { + req := user.NewDescribeUserRequestById("2") + ins, err := impl.DescribeUser(ctx, req) + if err != nil { + t.Fatal(err) + } + t.Log(ins) +} + +// SELECT * FROM `users` WHERE username = 'admin' ORDER BY `users`.`id` LIMIT 1 +func TestDescribeUserRequestByName(t *testing.T) { + req := user.NewDescribeUserRequestByUserName("admin") + ins, err := impl.DescribeUser(ctx, req) + if err != nil { + t.Fatal(err) + } + t.Log(ins) + + err = ins.CheckPassword("1234561") + if err != nil { + t.Fatal(err) + } +} + +func TestUserJson(t *testing.T) { + u := user.NewUser(user.NewCreateUserRequest()) + t.Log(u) +} diff --git a/devcloud/mcenter/apps/user/model.go b/devcloud/mcenter/apps/user/model.go index 03448de..219f979 100644 --- a/devcloud/mcenter/apps/user/model.go +++ b/devcloud/mcenter/apps/user/model.go @@ -1,10 +1,11 @@ package user import ( - "encoding/json" "fmt" "time" + "github.com/infraboard/mcube/v2/exception" + "github.com/infraboard/mcube/v2/tools/pretty" "github.com/infraboard/modules/iam/apps" "golang.org/x/crypto/bcrypt" ) @@ -30,13 +31,18 @@ type User struct { } func (u *User) String() string { - dj, _ := json.Marshal(u) - return string(dj) + return pretty.ToJSON(u) } // 判断该用户的密码是否正确 func (u *User) CheckPassword(password string) error { - return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) + // u.Password hash过后的只 + // (password 原始值 + hash值中提区salt) + err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) + if err != nil { + return exception.NewUnauthorized("用户名或者密码对正确") + } + return nil } // 声明你这个对象存储在users表里面 diff --git a/devcloud/mcenter/test/set_up.go b/devcloud/mcenter/test/set_up.go new file mode 100644 index 0000000..6337f83 --- /dev/null +++ b/devcloud/mcenter/test/set_up.go @@ -0,0 +1,17 @@ +package test + +import ( + "os" + + "github.com/infraboard/mcube/v2/ioc" + // 要注册哪些对象, Book, Comment + + // 加载的业务对象 + _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps" +) + +func DevelopmentSet() { + // import 后自动执行的逻辑 + // 工具对象的初始化, 需要的是绝对路径 + ioc.DevelopmentSetupWithPath(os.Getenv("CONFIG_PATH")) +} diff --git a/go.mod b/go.mod index fb629ec..f62d8ec 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,11 @@ require ( github.com/caarlos0/env/v6 v6.10.1 github.com/gin-gonic/gin v1.10.0 github.com/infraboard/mcube/v2 v2.0.59 + github.com/infraboard/modules v0.0.12 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.10.0 + golang.org/x/crypto v0.38.0 + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.26.0 @@ -83,7 +86,6 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/arch v0.15.0 // indirect - golang.org/x/crypto v0.38.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.14.0 // indirect golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index c2c67b2..18946bd 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/infraboard/mcube/v2 v2.0.59 h1:NONiCPjN6xlbGCJx8+e+ZYZfXV58JByEMlzQ6ZZ+pXk= github.com/infraboard/mcube/v2 v2.0.59/go.mod h1:TbYs8cnD8Cg19sTdU0D+vqWAN+LzoxhMYWmAC2pfJkQ= +github.com/infraboard/modules v0.0.12 h1:vQqm+JwzmhL+hcD9SV+WVlp9ecInc7NsbGahcTmJ0Wk= +github.com/infraboard/modules v0.0.12/go.mod h1:NdgdH/NoeqibJmFPn9th+tisMuR862/crbXeH4FPMaU= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -197,6 +199,8 @@ golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=