From 19ad6625e672af777c381b3f7026e8ec93f96449 Mon Sep 17 00:00:00 2001 From: eson <9673575+githubcontent@user.noreply.gitee.com> Date: Thu, 27 Jul 2023 10:18:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- goctl_template/api/context.tpl | 32 ---- .../internal/logic/usergoogleloginlogic.go | 15 +- server/auth/internal/logic/userloginlogic.go | 7 +- server/auth/internal/svc/servicecontext.go | 4 + .../internal/handler/getcloudlisthandler.go | 2 +- utils/auth/confirmation_link.go | 146 ++++++++++++++++++ utils/auth/confirmation_link_test.go | 80 ++++++++++ utils/auth/register.go | 39 +---- utils/auth/user.go | 29 +++- utils/auth/user_test.go | 16 +- utils/basic/basic.go | 2 + utils/basic/request_parse.go | 14 +- 12 files changed, 301 insertions(+), 85 deletions(-) create mode 100644 utils/auth/confirmation_link.go create mode 100644 utils/auth/confirmation_link_test.go diff --git a/goctl_template/api/context.tpl b/goctl_template/api/context.tpl index 233d2023..b9b97827 100644 --- a/goctl_template/api/context.tpl +++ b/goctl_template/api/context.tpl @@ -34,36 +34,4 @@ func NewServiceContext(c {{.config}}) *ServiceContext { AllModels: gmodel.NewAllModels(initalize.InitMysql(c.SourceMysql)), {{.middlewareAssignment}} } -} - -func (svcCtx *ServiceContext) ParseJwtToken(r *http.Request) (jwt.MapClaims, error) { - AuthKey := r.Header.Get("Authorization") - if AuthKey == "" { - return nil, nil - } - AuthKey = AuthKey[7:] - - - if len(AuthKey) <= 50 { - return nil, errors.New(fmt.Sprint("Error parsing token, len:", len(AuthKey))) - } - - token, err := jwt.Parse(AuthKey, func(token *jwt.Token) (interface{}, error) { - // 检查签名方法是否为 HS256 - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - // 返回用于验证签名的密钥 - return []byte(svcCtx.Config.Auth.AccessSecret), nil - }) - if err != nil { - return nil, errors.New(fmt.Sprint("Error parsing token:", err)) - } - - // 验证成功返回 - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - return claims, nil - } - - return nil, errors.New(fmt.Sprint("Invalid token", err)) } \ No newline at end of file diff --git a/server/auth/internal/logic/usergoogleloginlogic.go b/server/auth/internal/logic/usergoogleloginlogic.go index 60aa28f6..de80f5b3 100644 --- a/server/auth/internal/logic/usergoogleloginlogic.go +++ b/server/auth/internal/logic/usergoogleloginlogic.go @@ -31,7 +31,7 @@ type UserGoogleLoginLogic struct { isRegistered bool // 是否注册 registerToken string // 注册邮箱的token - oauthinfo *auth.OAuthInfo + registerInfo *auth.RegisterToken } func NewUserGoogleLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserGoogleLoginLogic { @@ -52,19 +52,11 @@ func (l *UserGoogleLoginLogic) AfterLogic(w http.ResponseWriter, r *http.Request if resp.Code == 200 { if !l.isRegistered { - now := time.Now() - rtoken, err := auth.GenerateRegisterToken( - &l.svcCtx.Config.Auth.AccessSecret, - l.svcCtx.Config.Auth.AccessExpire, - now.Unix(), - l.oauthinfo.Id, - l.oauthinfo.Platform, - ) + rtoken, err := l.svcCtx.TokenManger.Encrypt(l.registerInfo) if err != nil { resp.SetStatus(basic.CodeOAuthRegisterTokenErr) } - l.registerToken = rtoken } @@ -165,7 +157,8 @@ func (l *UserGoogleLoginLogic) UserGoogleLogin(req *types.RequestGoogleLogin, us // 如果密码匹配,则生成 JWT Token。 nowSec := time.Now().Unix() - jwtToken, err := auth.GenerateJwtToken(&l.svcCtx.Config.Auth.AccessSecret, l.svcCtx.Config.Auth.AccessExpire, nowSec, user.Id, 0) + + jwtToken, err := auth.GenerateJwtTokenUint64(auth.StringToHash(*user.PasswordHash), l.svcCtx.Config.Auth.AccessExpire, nowSec, user.Id, 0) // 如果生成 JWT Token 失败,则抛出错误并返回未认证的状态码。 if err != nil { diff --git a/server/auth/internal/logic/userloginlogic.go b/server/auth/internal/logic/userloginlogic.go index 01830718..2293d650 100644 --- a/server/auth/internal/logic/userloginlogic.go +++ b/server/auth/internal/logic/userloginlogic.go @@ -68,8 +68,13 @@ func (l *UserLoginLogic) UserLogin(req *types.RequestUserLogin, userinfo *auth.U // 如果密码匹配,则生成 JWT Token。 nowSec := time.Now().Unix() + us, err := l.svcCtx.SharedState.GetUserState(user.Id) + if err != nil { + logx.Error(err) + return resp.SetStatus(basic.CodeSharedStateErr) + } - jwtToken, err := auth.GenerateJwtToken(&l.svcCtx.Config.Auth.AccessSecret, l.svcCtx.Config.Auth.AccessExpire, nowSec, user.Id, 0) + jwtToken, err := auth.GenerateJwtTokenUint64(us.PwdHash, l.svcCtx.Config.Auth.AccessExpire, nowSec, user.Id, 0) // 如果生成 JWT Token 失败,则抛出错误并返回未认证的状态码。 if err != nil { diff --git a/server/auth/internal/svc/servicecontext.go b/server/auth/internal/svc/servicecontext.go index ebb0ff05..0680840f 100644 --- a/server/auth/internal/svc/servicecontext.go +++ b/server/auth/internal/svc/servicecontext.go @@ -5,6 +5,7 @@ import ( "fmt" "fusenapi/fsm" "fusenapi/server/auth/internal/config" + "fusenapi/utils/auth" "fusenapi/utils/autoconfig" "net/http" @@ -21,6 +22,8 @@ type ServiceContext struct { MysqlConn *gorm.DB AllModels *gmodel.AllModelsGen + + TokenManger *auth.ConfirmationLink[auth.RegisterToken] } func NewServiceContext(c config.Config) *ServiceContext { @@ -32,6 +35,7 @@ func NewServiceContext(c config.Config) *ServiceContext { MysqlConn: conn, SharedState: StateServer, AllModels: gmodel.NewAllModels(initalize.InitMysql(c.SourceMysql)), + TokenManger: auth.NewConfirmationLink[auth.RegisterToken]([]byte(c.Auth.AccessSecret), "http://localhost:9900/api/auth/oauth2/register"), } } diff --git a/server/inventory/internal/handler/getcloudlisthandler.go b/server/inventory/internal/handler/getcloudlisthandler.go index 90708969..c6b5badc 100644 --- a/server/inventory/internal/handler/getcloudlisthandler.go +++ b/server/inventory/internal/handler/getcloudlisthandler.go @@ -15,7 +15,7 @@ func GetCloudListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.GetCloudListReq - userinfo, err := basic.RequestParse(w, r, svcCtx, &req) + userinfo, err := basic.RequestParse(w, r, svcCtx.SharedState, &req) if err != nil { return } diff --git a/utils/auth/confirmation_link.go b/utils/auth/confirmation_link.go new file mode 100644 index 00000000..ef0caf49 --- /dev/null +++ b/utils/auth/confirmation_link.go @@ -0,0 +1,146 @@ +package auth + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/gob" + "fmt" + "net/url" +) + +type ConfirmationLink[T any] struct { + Secret []byte + DefaultQueryKey string // 默认key 是 token + link *url.URL +} + +func NewConfirmationLink[T any](key []byte, UrlStr string) *ConfirmationLink[T] { + u, err := url.Parse(UrlStr) + if err != nil { + panic(err) + } + + return &ConfirmationLink[T]{ + Secret: key, + DefaultQueryKey: "token", + link: u, + } +} + +// Generate 序列化链接传入需求的obj +func (cl *ConfirmationLink[T]) Generate(obj *T) (string, error) { + + token, err := cl.Encrypt(obj) + if err != nil { + return "", err + } + + return cl.GenerateWithToken(token) +} + +// GenerateWithToken 序列化url带token +func (cl *ConfirmationLink[T]) GenerateWithToken(token string) (string, error) { + + q := cl.link.Query() + if q.Has(cl.DefaultQueryKey) { + q.Set(cl.DefaultQueryKey, token) + } else { + q.Add(cl.DefaultQueryKey, token) + } + + // 生成确认链接 + cl.link.RawQuery = q.Encode() + + return cl.link.String(), nil +} + +func fusenMakeKey(keysting string) []byte { + + key := []byte(keysting) + + var result [32]byte + + // If key length is more than 32, truncate it + if len(key) > 32 { + key = key[:32] + } + + // If key length is less than 32, replicate it until it reaches 32 + for len(key) < 32 { + key = append(key, key...) + } + + // Only take the first 32 bytes + key = key[:32] + + // Swap the first 16 bytes with the last 16 bytes + copy(result[:], key[16:]) + copy(result[16:], key[:16]) + + return result[:] +} + +func (cl *ConfirmationLink[T]) Encrypt(obj *T) (string, error) { + + var buf = bytes.NewBuffer(nil) + err := gob.NewEncoder(buf).Encode(obj) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(cl.Secret) + if err != nil { + return "", err + } + + nonce := make([]byte, 12) + // if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + // return "", err + // } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + ciphertext := aesgcm.Seal(nonce, nonce, buf.Bytes(), nil) + + return base64.URLEncoding.EncodeToString(ciphertext), nil +} + +func (cl *ConfirmationLink[T]) Decrypt(ciphertext string) (*T, error) { + block, err := aes.NewCipher(cl.Secret) + if err != nil { + return nil, err + } + + ct, err := base64.URLEncoding.DecodeString(ciphertext) + if err != nil { + return nil, err + } + + if len(ct) < 12 { + return nil, fmt.Errorf("ciphertext too short") + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + plaintext, err := aesgcm.Open(nil, ct[:12], ct[12:], nil) + if err != nil { + return nil, err + } + + // 解出golang的结构体 + var protected T + var buf = bytes.NewBuffer(plaintext) + err = gob.NewDecoder(buf).Decode(&protected) + if err != nil { + return nil, err + } + return &protected, nil +} diff --git a/utils/auth/confirmation_link_test.go b/utils/auth/confirmation_link_test.go new file mode 100644 index 00000000..67245fab --- /dev/null +++ b/utils/auth/confirmation_link_test.go @@ -0,0 +1,80 @@ +package auth + +import ( + "log" + "net/url" + "testing" + "time" + + "github.com/golang-jwt/jwt" +) + +func BenchmarkConfirmationLink(b *testing.B) { + + type Register struct { + Id int64 + Password string + platform string + Expired time.Time + } + + key := "21321321" + cl := NewConfirmationLink[Register](fusenMakeKey(key), "http://localhost:9900/api/auth/oauth2/register") + for i := 0; i < b.N; i++ { + + uri, _ := cl.Generate(&Register{Id: 39, Password: "21dsadsad", platform: "google", Expired: time.Now()}) + u, _ := url.Parse(uri) + token := u.Query()["token"] + cl.Decrypt(token[0]) + } +} + +func TestConfirmationLink(t *testing.T) { + + type Register struct { + Id int64 + Password string + platform string + Expired time.Time + } + + key := "21321321" + + cl := NewConfirmationLink[Register](fusenMakeKey(key), "http://localhost:9900/api/auth/oauth2/register") + uri, _ := cl.Generate(&Register{Id: 39, Password: "21dsadsad", platform: "google", Expired: time.Now()}) + log.Println(uri) + + u, _ := url.Parse(uri) + token := u.Query()["token"] + log.Println(cl.Decrypt(token[0])) +} + +const secret = "your-256-bit-secret" + +func BenchmarkJWT(b *testing.B) { + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + claims := &jwt.StandardClaims{ + ExpiresAt: time.Now().Unix() + 1020213021, + Issuer: "test", + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + ss, err := token.SignedString([]byte(secret)) + if err != nil { + b.Fatal(err) + } + + _, err = jwt.Parse(ss, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, jwt.ErrSignatureInvalid + } + return []byte(secret), nil + }) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/utils/auth/register.go b/utils/auth/register.go index ae586a99..b817a158 100644 --- a/utils/auth/register.go +++ b/utils/auth/register.go @@ -7,10 +7,18 @@ import ( "fmt" "net/http" "net/mail" + "time" "github.com/golang-jwt/jwt" ) +type RegisterToken struct { + Id int64 + Password string + Platform string + Expired time.Time +} + func ParseJwtTokenUint64SecretByRequest(r *http.Request, AccessSecret uint64) (jwt.MapClaims, error) { AuthKey := r.Header.Get("Authorization") if AuthKey == "" { @@ -93,37 +101,6 @@ func StringToHash(s string) uint64 { return intHash } -var secret = []byte("your-secret") - -// func generateConfirmationLink(id, email, password, name string, platform string) (string, error) { -// // 创建一个新的 JWT,并将用户的电子邮件设置为它的主题。 -// token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ -// "email": email, -// "password": password, -// "id": id, -// "platform": platform, -// "exp": time.Now().Add(24 * time.Hour).Unix(), // Token expires after 24 hours -// }) - -// // 签署 JWT。 -// tokenString, err := token.SignedString(secret) -// if err != nil { -// return "", err -// } - -// // 生成确认链接,这个链接包含 JWT。 -// link := url.URL{ -// Scheme: "http", -// Host: "yourserver.com", -// Path: "/confirm", -// RawQuery: url.Values{ -// "token": []string{tokenString}, -// }.Encode(), -// } - -// return link.String(), nil -// } - // func handleConfirm(w http.ResponseWriter, r *http.Request) { // // 从请求中获取 JWT。 // tokenString := r.URL.Query().Get("token") diff --git a/utils/auth/user.go b/utils/auth/user.go index 9d0e5a89..14b676e1 100644 --- a/utils/auth/user.go +++ b/utils/auth/user.go @@ -3,6 +3,7 @@ package auth import ( "crypto/sha256" "encoding/base64" + "encoding/binary" "errors" "fmt" @@ -127,6 +128,30 @@ func GetBackendUserInfoFormMapClaims(claims jwt.MapClaims) (*BackendUserInfo, er return userinfo, nil } +// GenerateJwtTokenUint64 网站jwt token生成 +func GenerateJwtTokenUint64(AccessSecret uint64, accessExpire, nowSec int64, userid int64, guestid int64) (string, error) { + claims := make(jwt.MapClaims) + claims["exp"] = nowSec + accessExpire + claims["iat"] = nowSec + + if userid == 0 && guestid == 0 { + err := errors.New("userid and guestid cannot be 0 at the same time") + logx.Error(err) + return "", err + + } + claims["user_id"] = userid + claims["guest_id"] = guestid + + token := jwt.New(jwt.SigningMethodHS256) + token.Claims = claims + + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, AccessSecret) + + return token.SignedString(key) +} + // GenerateJwtToken 网站jwt token生成 func GenerateJwtToken(accessSecret *string, accessExpire, nowSec int64, userid int64, guestid int64) (string, error) { claims := make(jwt.MapClaims) @@ -189,7 +214,9 @@ func getJwtClaims(AuthKey string, AccessSecret *string) (jwt.MapClaims, error) { } func PasswordHash(pwd string) string { - return base64.URLEncoding.EncodeToString(sha256.New().Sum([]byte(pwd))) + h := sha256.New() + h.Write([]byte(pwd)) + return base64.URLEncoding.EncodeToString(h.Sum(nil)) } func CheckValueRange[T comparable](v T, rangevalues ...T) bool { diff --git a/utils/auth/user_test.go b/utils/auth/user_test.go index 87a3c7bf..c6e11655 100644 --- a/utils/auth/user_test.go +++ b/utils/auth/user_test.go @@ -1,6 +1,9 @@ package auth import ( + "crypto/sha256" + "encoding/base64" + "fmt" "log" "testing" "time" @@ -54,7 +57,18 @@ func TestGenBackendJwt(t *testing.T) { } func TestCase1(t *testing.T) { - // log.Println(base64.URLEncoding.EncodeToString(sha256.New().Sum([]byte("fusen_backend_2023")))) + + a := sha256.New() + a.Write([]byte("fusen_backend_3021")) + base64.URLEncoding.EncodeToString(a.Sum(nil)) + as := fmt.Sprintf("%x", a.Sum(nil)) + + log.Println(as, len(as), base64.URLEncoding.EncodeToString(a.Sum(nil))) + + // b := sha256.New().Sum([]byte("fusen_backend_2022")) + // bs := fmt.Sprintf("%x", b) + // log.Println(bs) + // log.Println(as == bs) // log.Println(basic.CBCEncrypt("sdfsdfsadsasdasadas", "1232132131232132")) diff --git a/utils/basic/basic.go b/utils/basic/basic.go index 6a3c8349..16705eb9 100644 --- a/utils/basic/basic.go +++ b/utils/basic/basic.go @@ -74,6 +74,8 @@ var ( CodeAesCbcEncryptionErr = &StatusResponse{5106, "encryption data err"} // 加密数据失败 CodeAesCbcDecryptionErr = &StatusResponse{5107, "decryption data err"} // 解密数据失败 + + CodeSharedStateErr = &StatusResponse{5201, "shared state server err"} // 状态机错误 ) type Response struct { diff --git a/utils/basic/request_parse.go b/utils/basic/request_parse.go index da50cf17..c033f732 100644 --- a/utils/basic/request_parse.go +++ b/utils/basic/request_parse.go @@ -53,7 +53,7 @@ func NormalAfterLogic(w http.ResponseWriter, r *http.Request, resp *Response) { func RequestParse(w http.ResponseWriter, r *http.Request, state *fsm.StateCluster, LogicRequest any) (*auth.UserInfo, error) { - token, info, err := auth.ParseJwtTokenHeader[auth.UserInfo](r) + token, info, err := auth.ParseJwtTokenHeader[auth.UserInfo](r) //解析Token头, 和payload信息 if err != nil { logx.Error(err) return nil, err @@ -61,15 +61,15 @@ func RequestParse(w http.ResponseWriter, r *http.Request, state *fsm.StateCluste var secret uint64 = 0 if info.IsUser() { - us, err := state.GetUserState(info.UserId) + us, err := state.GetUserState(info.UserId) //获取缓存的用户状态 if err != nil { logx.Error(err) return nil, err } - secret = us.PwdHash + secret = us.PwdHash // 获取密码的hash做jwt, 便于重置密码的使用 } else if info.IsGuest() { - secret = DefaultJwtSecret + secret = DefaultJwtSecret //获取默认的hash } var userinfo *auth.UserInfo @@ -97,10 +97,10 @@ func RequestParse(w http.ResponseWriter, r *http.Request, state *fsm.StateCluste logx.Info("unauthorized:", err.Error()) return nil, err } - } else { - // 如果claims为nil,则认为用户身份为白板用户 - userinfo = &auth.UserInfo{UserId: 0, GuestId: 0} } + } else { + // 白板用户 + userinfo = &auth.UserInfo{UserId: 0, GuestId: 0} } // 如果端点有请求结构体,则使用httpx.Parse方法从HTTP请求体中解析请求数据