添加token颁发器
This commit is contained in:
parent
426e493967
commit
47e4772618
7
devcloud/.vscode/settings.json
vendored
Normal file
7
devcloud/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"go.testEnvVars": {
|
||||
"workspaceFolder": "${workspaceFolder}",
|
||||
"CONFIG_PATH": "${workspaceFolder}/etc/application.toml"
|
||||
},
|
||||
"go.testEnvFile": "${workspaceFolder}/etc/unit_test.env"
|
||||
}
|
0
devcloud/etc/unit_test.env
Normal file
0
devcloud/etc/unit_test.env
Normal file
@ -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"
|
||||
)
|
||||
|
1
devcloud/mcenter/apps/token/api/api.go
Normal file
1
devcloud/mcenter/apps/token/api/api.go
Normal file
@ -0,0 +1 @@
|
||||
package api
|
1
devcloud/mcenter/apps/token/api/token.go
Normal file
1
devcloud/mcenter/apps/token/api/token.go
Normal file
@ -0,0 +1 @@
|
||||
package api
|
21
devcloud/mcenter/apps/token/const.go
Normal file
21
devcloud/mcenter/apps/token/const.go
Normal file
@ -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)
|
||||
)
|
@ -29,3 +29,9 @@ const (
|
||||
// 异常Ip登陆
|
||||
LOCK_TYPE_OTHER_IP_LOGGED_IN
|
||||
)
|
||||
|
||||
type DESCRIBE_BY int
|
||||
|
||||
const (
|
||||
DESCRIBE_BY_ACCESS_TOKEN DESCRIBE_BY = iota
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
18
devcloud/mcenter/apps/token/impl/impl_test.go
Normal file
18
devcloud/mcenter/apps/token/impl/impl_test.go
Normal file
@ -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()
|
||||
}
|
@ -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
|
||||
// }
|
||||
|
27
devcloud/mcenter/apps/token/impl/token_test.go
Normal file
27
devcloud/mcenter/apps/token/impl/token_test.go
Normal file
@ -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)
|
||||
}
|
@ -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 {
|
||||
|
44
devcloud/mcenter/apps/token/issuer.go
Normal file
44
devcloud/mcenter/apps/token/issuer.go
Normal file
@ -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)
|
||||
}
|
12
devcloud/mcenter/apps/token/issuer_test.go
Normal file
12
devcloud/mcenter/apps/token/issuer_test.go
Normal file
@ -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))
|
||||
}
|
67
devcloud/mcenter/apps/token/issuers/password/issuer.go
Normal file
67
devcloud/mcenter/apps/token/issuers/password/issuer.go
Normal file
@ -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
|
||||
}
|
22
devcloud/mcenter/apps/token/issuers/password/issuer_test.go
Normal file
22
devcloud/mcenter/apps/token/issuers/password/issuer_test.go
Normal file
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
67
devcloud/mcenter/apps/token/issuers/private_token/issuer.go
Normal file
67
devcloud/mcenter/apps/token/issuers/private_token/issuer.go
Normal file
@ -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
|
||||
}
|
6
devcloud/mcenter/apps/token/issuers/registry.go
Normal file
6
devcloud/mcenter/apps/token/issuers/registry.go
Normal file
@ -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"
|
||||
)
|
@ -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 <Your API key>
|
||||
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 {
|
||||
|
@ -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))
|
||||
}
|
||||
```
|
||||
|
1
devcloud/mcenter/apps/user/api/api.go
Normal file
1
devcloud/mcenter/apps/user/api/api.go
Normal file
@ -0,0 +1 @@
|
||||
package api
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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表里面
|
||||
|
17
devcloud/mcenter/test/set_up.go
Normal file
17
devcloud/mcenter/test/set_up.go
Normal file
@ -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"))
|
||||
}
|
4
go.mod
4
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
|
||||
|
4
go.sum
4
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=
|
||||
|
Loading…
x
Reference in New Issue
Block a user