diff --git a/server/auth/auth.go b/server/auth/auth.go new file mode 100644 index 00000000..f1f3eca9 --- /dev/null +++ b/server/auth/auth.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + "time" + + "fusenapi/utils/auth" + + "fusenapi/server/auth/internal/config" + "fusenapi/server/auth/internal/handler" + "fusenapi/server/auth/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/rest" +) + +var configFile = flag.String("f", "etc/auth.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + c.Timeout = int64(time.Second * 15) + server := rest.MustNewServer(c.RestConf, rest.WithCustomCors(auth.FsCors, func(w http.ResponseWriter) { + })) + defer server.Stop() + + ctx := svc.NewServiceContext(c) + handler.RegisterHandlers(server, ctx) + + fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) + server.Start() +} diff --git a/server/auth/etc/auth.yaml b/server/auth/etc/auth.yaml new file mode 100644 index 00000000..fac66390 --- /dev/null +++ b/server/auth/etc/auth.yaml @@ -0,0 +1,19 @@ +Name: auth +Host: 0.0.0.0 +Port: 9980 +MainAddress: "http://localhost:9900" +SourceMysql: fusentest:XErSYmLELKMnf3Dh@tcp(110.41.19.98:3306)/fusentest +Auth: + AccessSecret: fusen2023 + AccessExpire: 2592000 + RefreshAfter: 1592000 + + +OAuth: + google: + appid: "1064842923358-e94msq2glj6qr4lrva9ts3hqjjt53q8h.apps.googleusercontent.com" + secret: "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" + + facebook: + appid: "1095953604597065" + secret: "b146872550a190d5275b1420c212002e" \ No newline at end of file diff --git a/server/auth/internal/config/config.go b/server/auth/internal/config/config.go new file mode 100644 index 00000000..679c8627 --- /dev/null +++ b/server/auth/internal/config/config.go @@ -0,0 +1,27 @@ +package config + +import ( + "fusenapi/server/auth/internal/types" + + "github.com/zeromicro/go-zero/rest" +) + +type Config struct { + rest.RestConf + SourceMysql string + Auth types.Auth + + MainAddress string + + OAuth struct { + Google struct { + Appid string + Secret string + } + + Facebook struct { + Appid string + Secret string + } + } +} diff --git a/server/auth/internal/handler/acceptcookiehandler.go b/server/auth/internal/handler/acceptcookiehandler.go new file mode 100644 index 00000000..3e0e8b44 --- /dev/null +++ b/server/auth/internal/handler/acceptcookiehandler.go @@ -0,0 +1,35 @@ +package handler + +import ( + "net/http" + "reflect" + + "fusenapi/utils/basic" + + "fusenapi/server/auth/internal/logic" + "fusenapi/server/auth/internal/svc" + "fusenapi/server/auth/internal/types" +) + +func AcceptCookieHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + var req types.Request + userinfo, err := basic.RequestParse(w, r, svcCtx, &req) + if err != nil { + return + } + + // 创建一个业务逻辑层实例 + l := logic.NewAcceptCookieLogic(r.Context(), svcCtx) + + rl := reflect.ValueOf(l) + basic.BeforeLogic(w, r, rl) + + resp := l.AcceptCookie(&req, userinfo) + + if !basic.AfterLogic(w, r, rl, resp) { + basic.NormalAfterLogic(w, r, resp) + } + } +} diff --git a/server/auth/internal/handler/routes.go b/server/auth/internal/handler/routes.go new file mode 100644 index 00000000..3e951654 --- /dev/null +++ b/server/auth/internal/handler/routes.go @@ -0,0 +1,37 @@ +// Code generated by goctl. DO NOT EDIT. +package handler + +import ( + "net/http" + + "fusenapi/server/auth/internal/svc" + + "github.com/zeromicro/go-zero/rest" +) + +func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/api/auth/login", + Handler: UserLoginHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/api/user/accept-cookie", + Handler: AcceptCookieHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/api/user/oauth2/login/google", + Handler: UserGoogleLoginHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/api/user/oauth2/login/register", + Handler: UserEmailRegisterHandler(serverCtx), + }, + }, + ) +} diff --git a/server/home-user-auth/internal/handler/useremailregisterhandler.go b/server/auth/internal/handler/useremailregisterhandler.go similarity index 81% rename from server/home-user-auth/internal/handler/useremailregisterhandler.go rename to server/auth/internal/handler/useremailregisterhandler.go index 5d274809..bf073982 100644 --- a/server/home-user-auth/internal/handler/useremailregisterhandler.go +++ b/server/auth/internal/handler/useremailregisterhandler.go @@ -6,9 +6,9 @@ import ( "fusenapi/utils/basic" - "fusenapi/server/home-user-auth/internal/logic" - "fusenapi/server/home-user-auth/internal/svc" - "fusenapi/server/home-user-auth/internal/types" + "fusenapi/server/auth/internal/logic" + "fusenapi/server/auth/internal/svc" + "fusenapi/server/auth/internal/types" ) func UserEmailRegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/server/home-user-auth/internal/handler/usergoogleloginhandler.go b/server/auth/internal/handler/usergoogleloginhandler.go similarity index 81% rename from server/home-user-auth/internal/handler/usergoogleloginhandler.go rename to server/auth/internal/handler/usergoogleloginhandler.go index 154b2450..fd1f54f6 100644 --- a/server/home-user-auth/internal/handler/usergoogleloginhandler.go +++ b/server/auth/internal/handler/usergoogleloginhandler.go @@ -6,9 +6,9 @@ import ( "fusenapi/utils/basic" - "fusenapi/server/home-user-auth/internal/logic" - "fusenapi/server/home-user-auth/internal/svc" - "fusenapi/server/home-user-auth/internal/types" + "fusenapi/server/auth/internal/logic" + "fusenapi/server/auth/internal/svc" + "fusenapi/server/auth/internal/types" ) func UserGoogleLoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/server/home-user-auth/internal/handler/userloginhandler.go b/server/auth/internal/handler/userloginhandler.go similarity index 80% rename from server/home-user-auth/internal/handler/userloginhandler.go rename to server/auth/internal/handler/userloginhandler.go index d08232d6..2e32b702 100644 --- a/server/home-user-auth/internal/handler/userloginhandler.go +++ b/server/auth/internal/handler/userloginhandler.go @@ -6,9 +6,9 @@ import ( "fusenapi/utils/basic" - "fusenapi/server/home-user-auth/internal/logic" - "fusenapi/server/home-user-auth/internal/svc" - "fusenapi/server/home-user-auth/internal/types" + "fusenapi/server/auth/internal/logic" + "fusenapi/server/auth/internal/svc" + "fusenapi/server/auth/internal/types" ) func UserLoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/server/auth/internal/logic/acceptcookielogic.go b/server/auth/internal/logic/acceptcookielogic.go new file mode 100644 index 00000000..730bd493 --- /dev/null +++ b/server/auth/internal/logic/acceptcookielogic.go @@ -0,0 +1,43 @@ +package logic + +import ( + "fusenapi/utils/auth" + "fusenapi/utils/basic" + + "context" + + "fusenapi/server/auth/internal/svc" + "fusenapi/server/auth/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AcceptCookieLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAcceptCookieLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AcceptCookieLogic { + return &AcceptCookieLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 处理进入前逻辑w,r +// func (l *AcceptCookieLogic) BeforeLogic(w http.ResponseWriter, r *http.Request) { +// } + +// 处理逻辑后 w,r 如:重定向, resp 必须重新处理 +// func (l *AcceptCookieLogic) AfterLogic(w http.ResponseWriter, r *http.Request, resp *basic.Response) { +// // httpx.OkJsonCtx(r.Context(), w, resp) +// } + +func (l *AcceptCookieLogic) AcceptCookie(req *types.Request, userinfo *auth.UserInfo) (resp *basic.Response) { + // 返回值必须调用Set重新返回, resp可以空指针调用 resp.SetStatus(basic.CodeOK, data) + // userinfo 传入值时, 一定不为null + + return resp.SetStatus(basic.CodeOK) +} diff --git a/server/home-user-auth/internal/logic/useremailregisterlogic.go b/server/auth/internal/logic/useremailregisterlogic.go similarity index 84% rename from server/home-user-auth/internal/logic/useremailregisterlogic.go rename to server/auth/internal/logic/useremailregisterlogic.go index eb276e5b..c8e4751f 100644 --- a/server/home-user-auth/internal/logic/useremailregisterlogic.go +++ b/server/auth/internal/logic/useremailregisterlogic.go @@ -6,8 +6,8 @@ import ( "context" - "fusenapi/server/home-user-auth/internal/svc" - "fusenapi/server/home-user-auth/internal/types" + "fusenapi/server/auth/internal/svc" + "fusenapi/server/auth/internal/types" "github.com/zeromicro/go-zero/core/logx" ) @@ -30,8 +30,9 @@ func NewUserEmailRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) // func (l *UserEmailRegisterLogic) BeforeLogic(w http.ResponseWriter, r *http.Request) { // } -// 处理逻辑后 w,r 如:重定向 +// 处理逻辑后 w,r 如:重定向, resp 必须重新处理 // func (l *UserEmailRegisterLogic) AfterLogic(w http.ResponseWriter, r *http.Request, resp *basic.Response) { +// // httpx.OkJsonCtx(r.Context(), w, resp) // } func (l *UserEmailRegisterLogic) UserEmailRegister(req *types.RequestEmailRegister, userinfo *auth.UserInfo) (resp *basic.Response) { diff --git a/server/auth/internal/logic/usergoogleloginlogic.go b/server/auth/internal/logic/usergoogleloginlogic.go new file mode 100644 index 00000000..3bdad6bf --- /dev/null +++ b/server/auth/internal/logic/usergoogleloginlogic.go @@ -0,0 +1,177 @@ +package logic + +import ( + "fmt" + "fusenapi/utils/auth" + "fusenapi/utils/basic" + "log" + "net/http" + "time" + + "context" + + "fusenapi/server/auth/internal/svc" + "fusenapi/server/auth/internal/types" + + "github.com/474420502/requests" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/rest/httpx" + "golang.org/x/net/proxy" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "gorm.io/gorm" +) + +type UserGoogleLoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext + + token string // 登录 token + + isRegistered bool // 是否注册 + registerToken string // 注册邮箱的token + oauthinfo *auth.OAuthInfo +} + +func NewUserGoogleLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserGoogleLoginLogic { + return &UserGoogleLoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 处理进入前逻辑w,r +// func (l *UserGoogleLoginLogic) BeforeLogic(w http.ResponseWriter, r *http.Request) { +// } + +// 处理逻辑后 w,r 如:重定向, resp 必须重新处理 +func (l *UserGoogleLoginLogic) AfterLogic(w http.ResponseWriter, r *http.Request, resp *basic.Response) { + + 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, + ) + + if err != nil { + resp.SetStatus(basic.CodeOAuthRegisterTokenErr) + } + + l.registerToken = rtoken + } + + rurl := fmt.Sprintf( + l.svcCtx.Config.MainAddress+"/oauth?token=%s&is_registered=%t®ister_token=%s", + l.token, + l.isRegistered, + l.registerToken, + ) + + html := fmt.Sprintf(` + + + + Redirect + + + + + + `, rurl) + fmt.Fprintln(w, html) + } else { + httpx.OkJson(w, resp) + } + +} + +func (l *UserGoogleLoginLogic) UserGoogleLogin(req *types.RequestGoogleLogin, userinfo *auth.UserInfo) (resp *basic.Response) { + // 返回值必须调用Set重新返回, resp可以空指针调用 resp.SetStatus(basic.CodeOK, data) + // userinfo 传入值时, 一定不为null + + dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:1080", nil, proxy.Direct) + if err != nil { + log.Fatal(err) + } + + customClient := &http.Client{ + Transport: &http.Transport{ + Dial: dialer.Dial, + }, + } + + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, customClient) + + var googleOauthConfig = &oauth2.Config{ + RedirectURL: "http://localhost:9900/api/user/oauth2/login/google", + ClientID: l.svcCtx.Config.OAuth.Google.Appid, + ClientSecret: l.svcCtx.Config.OAuth.Google.Secret, + Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}, + Endpoint: google.Endpoint, + } + + token, err := googleOauthConfig.Exchange(ctx, req.Code) + if err != nil { + logx.Error(err) + resp.SetStatus(basic.CodeApiErr) + } + ses := requests.NewSession() + ses.Config().SetProxy("socks5://127.0.0.1:1080") // 代理 为了测试功能 + + r, err := ses.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token.AccessToken).Execute() + if err != nil { + logx.Error(err) + return resp.SetStatus(basic.CodeOAuthGoogleApiErr) + } + + log.Println(r.Json()) + + googleId := r.Json().Get("id").Int() + user, err := l.svcCtx.AllModels.FsUser.FindUserByGoogleId(context.TODO(), googleId) + if err != nil { + if err != gorm.ErrRecordNotFound { + logx.Error(err) + return resp.SetStatus(basic.CodeDbSqlErr) + } + + // 进入邮件注册流程 + if req.Email == "" { + return resp.SetStatus(basic.CodeOK) + } + + // 这里是注册模块, 发邮件, 通过邮件注册确认邮箱存在 + + // 邮箱验证格式错误 + if !auth.ValidateEmail(req.Email) { + return resp.SetStatus(basic.CodeOAuthEmailErr) + } + + return resp.SetStatus(basic.CodeOK) + } + + // 如果密码匹配,则生成 JWT Token。 + nowSec := time.Now().Unix() + jwtToken, err := auth.GenerateJwtToken(&l.svcCtx.Config.Auth.AccessSecret, l.svcCtx.Config.Auth.AccessExpire, nowSec, user.Id, 0) + + // 如果生成 JWT Token 失败,则抛出错误并返回未认证的状态码。 + if err != nil { + logx.Error(err) + return resp.SetStatus(basic.CodeServiceErr) + } + + l.token = jwtToken + + return resp.SetStatus(basic.CodeOK) +} diff --git a/server/auth/internal/logic/userloginlogic.go b/server/auth/internal/logic/userloginlogic.go new file mode 100644 index 00000000..3fb705ca --- /dev/null +++ b/server/auth/internal/logic/userloginlogic.go @@ -0,0 +1,93 @@ +package logic + +import ( + "errors" + "fmt" + "fusenapi/utils/auth" + "fusenapi/utils/basic" + "net/http" + "time" + + "context" + + "fusenapi/server/auth/internal/svc" + "fusenapi/server/auth/internal/types" + + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/rest/httpx" + "gorm.io/gorm" +) + +type UserLoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext + + token string +} + +func NewUserLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLoginLogic { + return &UserLoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 处理进入前逻辑w,r +// func (l *UserLoginLogic) BeforeLogic(w http.ResponseWriter, r *http.Request) { +// } + +// 处理逻辑后 w,r 如:重定向, resp 必须重新处理 +func (l *UserLoginLogic) AfterLogic(w http.ResponseWriter, r *http.Request, resp *basic.Response) { + if l.token != "" { + w.Header().Add("Authorization", fmt.Sprintf("Bearer %s", l.token)) + } + + httpx.OkJsonCtx(r.Context(), w, resp) +} + +func (l *UserLoginLogic) UserLogin(req *types.RequestUserLogin, userinfo *auth.UserInfo) (resp *basic.Response) { + // 返回值必须调用Set重新返回, resp可以空指针调用 resp.SetStatus(basic.CodeOK, data) + // userinfo 传入值时, 一定不为null + + // 创建一个 FsUserModel 对象 m 并实例化之,该对象用于操作 MySQL 数据库中的用户数据表。 + m := l.svcCtx.AllModels.FsUser + + // 在用户数据表中根据登录名(email)查找用户记录,并返回 UserModel 类型的结构体对象 userModel。 + user, err := m.FindUserByEmail(l.ctx, req.Email) + if errors.Is(err, gorm.ErrRecordNotFound) { + return resp.SetStatus(basic.CodeEmailNotFoundErr) + } + + // 如果在用户数据表中找到了登录名匹配的用户记录,则判断密码是否匹配。 + if *user.PasswordHash != req.Password { + logx.Info("密码错误") + return resp.SetStatus(basic.CodePasswordErr) + } + + // 如果密码匹配,则生成 JWT Token。 + nowSec := time.Now().Unix() + jwtToken, err := auth.GenerateJwtToken(&l.svcCtx.Config.Auth.AccessSecret, l.svcCtx.Config.Auth.AccessExpire, nowSec, user.Id, 0) + + // 如果生成 JWT Token 失败,则抛出错误并返回未认证的状态码。 + if err != nil { + logx.Error(err) + return resp.SetStatus(basic.CodeUnAuth) + } + + // 如果更新 VerificationToken 字段失败,则返回未认证的状态码。 + if err != nil { + return resp.SetStatus(basic.CodeUnAuth) + } + + // 构造 DataUserLogin 类型的数据对象 data 并设置其属性值为生成的 JWT Token。 + data := &types.DataUserLogin{ + Token: jwtToken, + } + + l.token = jwtToken + + // 返回认证成功的状态码以及数据对象 data 和 JWT Token。 + return resp.SetStatus(basic.CodeOK, data) +} diff --git a/server/auth/internal/svc/servicecontext.go b/server/auth/internal/svc/servicecontext.go new file mode 100644 index 00000000..9841ab05 --- /dev/null +++ b/server/auth/internal/svc/servicecontext.go @@ -0,0 +1,61 @@ +package svc + +import ( + "errors" + "fmt" + "fusenapi/server/auth/internal/config" + "net/http" + + "fusenapi/initalize" + "fusenapi/model/gmodel" + + "github.com/golang-jwt/jwt" + "gorm.io/gorm" +) + +type ServiceContext struct { + Config config.Config + + MysqlConn *gorm.DB + AllModels *gmodel.AllModelsGen +} + +func NewServiceContext(c config.Config) *ServiceContext { + + return &ServiceContext{ + Config: c, + MysqlConn: initalize.InitMysql(c.SourceMysql), + AllModels: gmodel.NewAllModels(initalize.InitMysql(c.SourceMysql)), + } +} + +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)) +} diff --git a/server/auth/internal/types/types.go b/server/auth/internal/types/types.go new file mode 100644 index 00000000..ccb83ca7 --- /dev/null +++ b/server/auth/internal/types/types.go @@ -0,0 +1,101 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +import ( + "fusenapi/utils/basic" +) + +type RequestUserLogin struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type RequestGoogleLogin struct { + Code string `form:"code"` + Scope string `form:"scope"` + AuthUser string `form:"authuser"` + Prompt string `form:"prompt"` + Email string `form:"email,optional"` +} + +type RequestEmailRegister struct { + Email string `json:"email"` + RegisterToken string `json:"register_token"` +} + +type DataUserLogin struct { + Token string `json:"token"` // 登录jwt token +} + +type DataGuest struct { + Token string `json:"token"` // 登录jwt token +} + +type Request struct { +} + +type Response struct { + Code int `json:"code"` + Message string `json:"msg"` + Data interface{} `json:"data"` +} + +type Auth struct { + AccessSecret string `json:"accessSecret"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type File struct { + Filename string `fsfile:"filename"` + Header map[string][]string `fsfile:"header"` + Size int64 `fsfile:"size"` + Data []byte `fsfile:"data"` +} + +type Meta struct { + TotalCount int64 `json:"totalCount"` + PageCount int64 `json:"pageCount"` + CurrentPage int `json:"currentPage"` + PerPage int `json:"perPage"` +} + +// Set 设置Response的Code和Message值 +func (resp *Response) Set(Code int, Message string) *Response { + return &Response{ + Code: Code, + Message: Message, + } +} + +// Set 设置整个Response +func (resp *Response) SetWithData(Code int, Message string, Data interface{}) *Response { + return &Response{ + Code: Code, + Message: Message, + Data: Data, + } +} + +// SetStatus 设置默认StatusResponse(内部自定义) 默认msg, 可以带data, data只使用一个参数 +func (resp *Response) SetStatus(sr *basic.StatusResponse, data ...interface{}) *Response { + newResp := &Response{ + Code: sr.Code, + } + if len(data) == 1 { + newResp.Data = data[0] + } + return newResp +} + +// SetStatusWithMessage 设置默认StatusResponse(内部自定义) 非默认msg, 可以带data, data只使用一个参数 +func (resp *Response) SetStatusWithMessage(sr *basic.StatusResponse, msg string, data ...interface{}) *Response { + newResp := &Response{ + Code: sr.Code, + Message: msg, + } + if len(data) == 1 { + newResp.Data = data[0] + } + return newResp +} diff --git a/server/home-user-auth/etc/home-user-auth.yaml b/server/home-user-auth/etc/home-user-auth.yaml index 7ef5a2c9..a35c3ac8 100644 --- a/server/home-user-auth/etc/home-user-auth.yaml +++ b/server/home-user-auth/etc/home-user-auth.yaml @@ -1,7 +1,6 @@ Name: home-user-auth Host: 0.0.0.0 Port: 9904 -MainAddress: "http://localhost:9900" SourceMysql: fusentest:XErSYmLELKMnf3Dh@tcp(110.41.19.98:3306)/fusentest Auth: @@ -9,14 +8,5 @@ Auth: AccessExpire: 2592000 RefreshAfter: 1592000 -OAuth: - google: - appid: "1064842923358-e94msq2glj6qr4lrva9ts3hqjjt53q8h.apps.googleusercontent.com" - secret: "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" - - facebook: - appid: "1095953604597065" - secret: "b146872550a190d5275b1420c212002e" - Stripe: SK: "sk_test_51IisojHygnIJZeghPVSBhkwySfcyDV4SoAduIxu3J7bvSJ9cZMD96LY1LO6SpdbYquLJX5oKvgEBB67KT9pecfCy00iEC4pp9y" diff --git a/server/home-user-auth/internal/handler/routes.go b/server/home-user-auth/internal/handler/routes.go index 75ac9d8f..230527b4 100644 --- a/server/home-user-auth/internal/handler/routes.go +++ b/server/home-user-auth/internal/handler/routes.go @@ -12,16 +12,6 @@ import ( func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( []rest.Route{ - { - Method: http.MethodPost, - Path: "/api/user/login", - Handler: UserLoginHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/api/user/accept-cookie", - Handler: AcceptCookieHandler(serverCtx), - }, { Method: http.MethodGet, Path: "/api/user/fonts", @@ -67,16 +57,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/api/user/order-delete", Handler: UserOderDeleteHandler(serverCtx), }, - { - Method: http.MethodGet, - Path: "/api/user/oauth2/login/google", - Handler: UserGoogleLoginHandler(serverCtx), - }, - { - Method: http.MethodGet, - Path: "/api/user/oauth2/login/register", - Handler: UserEmailRegisterHandler(serverCtx), - }, { Method: http.MethodGet, Path: "/api/user/order-list", diff --git a/server/home-user-auth/internal/logic/email_manager.go b/server/home-user-auth/internal/logic/email_manager.go new file mode 100644 index 00000000..60e600af --- /dev/null +++ b/server/home-user-auth/internal/logic/email_manager.go @@ -0,0 +1,161 @@ +package logic + +import ( + "bytes" + "log" + "net/smtp" + "sync" + "text/template" + "time" +) + +var EmailManager *EmailSender + +// EmailSender +type EmailSender struct { + lock sync.Mutex + EmailTasks chan string // 处理email的队列 + Auth smtp.Auth // 邮箱发送处理 + FromEmail string // 发送的email, 公司email + emailSending map[string]*EmailTask // 正在发送的邮件 + ResendTimeLimit time.Duration // 重发时间限制 +} + +// EmailTask +type EmailTask struct { + Email string // email + SendTime time.Time // 处理的任务时间 +} + +// ProcessEmailTasks 处理邮件队列 +func (m *EmailSender) ProcessEmailTasks() { + for { + emailTarget, ok := <-m.EmailTasks + if !ok { + log.Println("Email task channel closed") + break + } + + m.lock.Lock() + _, isSending := m.emailSending[emailTarget] + if isSending { + m.lock.Unlock() + continue + } + + m.emailSending[emailTarget] = &EmailTask{ + Email: emailTarget, + SendTime: time.Now(), + } + m.lock.Unlock() + + // TODO: Replace with actual email content + content := []byte("Hello, this is a test email") + err := smtp.SendMail(emailTarget, m.Auth, m.FromEmail, []string{emailTarget}, content) + if err != nil { + log.Printf("Failed to send email to %s: %v\n", emailTarget, err) + m.Resend(emailTarget, content) + } + } +} + +// Resend 重发邮件 +func (m *EmailSender) Resend(emailTarget string, content []byte) { + time.Sleep(m.ResendTimeLimit) + + m.lock.Lock() + defer m.lock.Unlock() + + // Check if the email task still exists and has not been sent successfully + if task, ok := m.emailSending[emailTarget]; ok && task.SendTime.Add(m.ResendTimeLimit).After(time.Now()) { + err := smtp.SendMail(emailTarget, m.Auth, m.FromEmail, []string{emailTarget}, content) + if err != nil { + log.Printf("Failed to resend email to %s: %v\n", emailTarget, err) + } else { + delete(m.emailSending, emailTarget) + } + } +} + +// ClearExpiredTasks 清除过期的邮件任务 +func (m *EmailSender) ClearExpiredTasks() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + <-ticker.C + + m.lock.Lock() + for email, task := range m.emailSending { + if task.SendTime.Add(m.ResendTimeLimit).Before(time.Now()) { + delete(m.emailSending, email) + } + } + m.lock.Unlock() + } +} + +func init() { + + // Initialize the email manager + EmailManager = &EmailSender{ + EmailTasks: make(chan string, 10), + Auth: smtp.PlainAuth( + "", + "user@example.com", + "password", + "smtp.gmail.com", + ), + FromEmail: "user@example.com", + emailSending: make(map[string]*EmailTask, 10), + ResendTimeLimit: time.Minute * 1, + } + + // Start processing email tasks + go EmailManager.ProcessEmailTasks() + + // Start clearing expired tasks + go EmailManager.ClearExpiredTasks() +} + +const emailTemplate = `Subject: Your {{.CompanyName}} Account Confirmation + +Dear + +Thank you for creating an account with {{.CompanyName}}. We're excited to have you on board! + +Before we get started, we just need to confirm that this is the right email address. Please confirm your email address by clicking on the link below: + +{{.ConfirmationLink}} + +Once you've confirmed, you can get started with {{.CompanyName}}. If you have any questions, feel free to reply to this email. We're here to help! + +If you did not create an account with us, please ignore this email. + +Thanks, +{{.SenderName}} +{{.SenderTitle}} +{{.CompanyName}} +` + +func RenderEmailTemplate(companyName, recipient, confirmationLink, senderName, senderTitle string) string { + tmpl, err := template.New("email").Parse(emailTemplate) + if err != nil { + log.Fatal(err) + } + + data := map[string]string{ + "CompanyName": companyName, + "ConfirmationLink": confirmationLink, + "SenderName": senderName, + "SenderTitle": senderTitle, + } + + var result bytes.Buffer + err = tmpl.Execute(&result, data) + if err != nil { + log.Fatal(err) + } + + return result.String() +} diff --git a/server/home-user-auth/internal/logic/usergoogleloginlogic.go b/server/home-user-auth/internal/logic/usergoogleloginlogic.go index 8c31f753..7ad4958a 100644 --- a/server/home-user-auth/internal/logic/usergoogleloginlogic.go +++ b/server/home-user-auth/internal/logic/usergoogleloginlogic.go @@ -6,7 +6,6 @@ import ( "fusenapi/utils/basic" "log" "net/http" - "net/url" "time" "context" @@ -16,6 +15,7 @@ import ( "github.com/474420502/requests" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/rest/httpx" "golang.org/x/net/proxy" "golang.org/x/oauth2" "golang.org/x/oauth2/google" @@ -31,6 +31,7 @@ type UserGoogleLoginLogic struct { isRegistered bool // 是否注册 registerToken string // 注册邮箱的token + oauthinfo *auth.OAuthInfo } func NewUserGoogleLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserGoogleLoginLogic { @@ -47,29 +48,52 @@ func NewUserGoogleLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *U func (l *UserGoogleLoginLogic) AfterLogic(w http.ResponseWriter, r *http.Request, resp *basic.Response) { - rurl := fmt.Sprintf( - l.svcCtx.Config.MainAddress+"/oauth?token=%s&is_registered=%t®ister_token=%s", - l.token, - l.isRegistered, - l.registerToken, - ) + if resp.Code == 200 { - html := fmt.Sprintf(` - - - - Redirect - - - - - - `, rurl) - fmt.Fprintln(w, html) + + l.registerToken = rtoken + } + + rurl := fmt.Sprintf( + l.svcCtx.Config.MainAddress+"/oauth?token=%s&is_registered=%t®ister_token=%s", + l.token, + l.isRegistered, + l.registerToken, + ) + + html := fmt.Sprintf(` + + + + Redirect + + + + + + `, rurl) + fmt.Fprintln(w, html) + } else { + httpx.OkJson(w, resp) + } + } func (l *UserGoogleLoginLogic) UserGoogleLogin(req *types.RequestGoogleLogin, userinfo *auth.UserInfo) (resp *basic.Response) { @@ -114,36 +138,39 @@ func (l *UserGoogleLoginLogic) UserGoogleLogin(req *types.RequestGoogleLogin, us log.Println(r.Json()) googleId := r.Json().Get("id").Int() - - // l.redirectUrl = "http://localhost:9900/oauth?token=21321123&is_registered" - // return resp.Set(304, "21321321") user, err := l.svcCtx.AllModels.FsUser.FindUserByGoogleId(context.TODO(), googleId) - log.Println(user) if err != nil { if err != gorm.ErrRecordNotFound { logx.Error(err) return resp.SetStatus(basic.CodeDbSqlErr) } + // 进入邮件注册流程 if req.Email == "" { return resp.SetStatus(basic.CodeOK) } - // 如果密码匹配,则生成 JWT Token。 - nowSec := time.Now().Unix() - jwtToken, err := auth.GenerateJwtToken(&l.svcCtx.Config.Auth.AccessSecret, l.svcCtx.Config.Auth.AccessExpire, nowSec, 0, 0) + // 这里是注册模块, 发邮件, 通过邮件注册确认邮箱存在 - // 如果生成 JWT Token 失败,则抛出错误并返回未认证的状态码。 - if err != nil { - logx.Error(err) - return resp.SetStatus(basic.CodeServiceErr) + // 邮箱验证格式错误 + if !auth.ValidateEmail(req.Email) { + return resp.SetStatus(basic.CodeOAuthEmailErr) } - return resp.SetRewriteHandler(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "http://localhost:9900?token="+url.QueryEscape(jwtToken), http.StatusFound) - }) - + return resp.SetStatus(basic.CodeOK) } + // 如果密码匹配,则生成 JWT Token。 + nowSec := time.Now().Unix() + jwtToken, err := auth.GenerateJwtToken(&l.svcCtx.Config.Auth.AccessSecret, l.svcCtx.Config.Auth.AccessExpire, nowSec, user.Id, 0) + + // 如果生成 JWT Token 失败,则抛出错误并返回未认证的状态码。 + if err != nil { + logx.Error(err) + return resp.SetStatus(basic.CodeServiceErr) + } + + l.token = jwtToken + return resp.SetStatus(basic.CodeOK) } diff --git a/server/home-user-auth/internal/types/types.go b/server/home-user-auth/internal/types/types.go index cdb5446a..bf8211ab 100644 --- a/server/home-user-auth/internal/types/types.go +++ b/server/home-user-auth/internal/types/types.go @@ -70,19 +70,6 @@ type Product struct { IsStop int64 `json:"is_stop"` } -type RequestGoogleLogin struct { - Code string `form:"code"` - Scope string `form:"scope"` - AuthUser string `form:"authuser"` - Prompt string `form:"prompt"` - Email string `form:"email,optional"` -} - -type RequestEmailRegister struct { - Email string `json:"email"` - RegisterToken string `json:"register_token"` -} - type RequestContactService struct { Type string `json:"type"` // 类型 RelationID int64 `json:"relation_id"` // 关系id @@ -108,11 +95,6 @@ type RequestBasicInfoForm struct { IsRemoveBg int64 `json:"is_remove_bg"` // 用户上传logo是否去除背景 } -type RequestUserLogin struct { - Email string `json:"email"` - Password string `json:"password"` -} - type RequestAddAddress struct { Id int64 `json:"id"` // address_id 地址id IsDefault int64 `json:"is_default"` //是否默认 diff --git a/server_api/auth.api b/server_api/auth.api new file mode 100644 index 00000000..2460b9b6 --- /dev/null +++ b/server_api/auth.api @@ -0,0 +1,53 @@ +syntax = "v1" + +info ( + title: // TODO: add title + desc: // TODO: add description + author: "" + email: "" +) + +import "basic.api" + +service auth { + @handler UserLoginHandler + post /api/auth/login(RequestUserLogin) returns (response); + + @handler AcceptCookieHandler + post /api/user/accept-cookie(request) returns (response); + + @handler UserGoogleLoginHandler + get /api/user/oauth2/login/google(RequestGoogleLogin) returns (response); + + @handler UserEmailRegisterHandler + get /api/user/oauth2/login/register(RequestEmailRegister) returns (response); +} + +// UserAddAddressHandler 用户登录请求结构 +type RequestUserLogin { + Email string `json:"email"` + Password string `json:"password"` +} + +type RequestGoogleLogin { + Code string `form:"code"` + Scope string `form:"scope"` + AuthUser string `form:"authuser"` + Prompt string `form:"prompt"` + Email string `form:"email,optional"` +} + +type RequestEmailRegister { + Email string `json:"email"` + RegisterToken string `json:"register_token"` +} + +// UserLoginHandler 用户登录请求结构 +type DataUserLogin { + Token string `json:"token"` // 登录jwt token +} + +// DataGuest 游客获取toekn请求结构 +type DataGuest { + Token string `json:"token"` // 登录jwt token +} \ No newline at end of file diff --git a/server_api/home-user-auth.api b/server_api/home-user-auth.api index fdc7594e..d36da50b 100644 --- a/server_api/home-user-auth.api +++ b/server_api/home-user-auth.api @@ -14,12 +14,6 @@ service home-user-auth { // @handler UserRegisterHandler // post /api/user/register(RequestUserRegister) returns (response); - @handler UserLoginHandler - post /api/user/login(RequestUserLogin) returns (response); - - @handler AcceptCookieHandler - post /api/user/accept-cookie(request) returns (response); - @handler UserFontsHandler get /api/user/fonts(request) returns (response); @@ -50,12 +44,6 @@ service home-user-auth { @handler UserOderDeleteHandler post /api/user/order-delete(RequestOrderId) returns (response); - @handler UserGoogleLoginHandler - get /api/user/oauth2/login/google(RequestGoogleLogin) returns (response); - - @handler UserEmailRegisterHandler - get /api/user/oauth2/login/register(RequestEmailRegister) returns (response); - //订单列表 @handler UserOrderListHandler get /api/user/order-list (UserOrderListReq) returns (response); @@ -136,19 +124,6 @@ type Product { IsStop int64 `json:"is_stop"` } -type RequestGoogleLogin { - Code string `form:"code"` - Scope string `form:"scope"` - AuthUser string `form:"authuser"` - Prompt string `form:"prompt"` - Email string `form:"email,optional"` -} - -type RequestEmailRegister { - Email string `json:"email"` - RegisterToken string `json:"register_token"` -} - type RequestContactService { Type string `json:"type"` // 类型 RelationID int64 `json:"relation_id"` // 关系id @@ -176,12 +151,6 @@ type RequestBasicInfoForm { // NewPassword string `form:"new_password,optional" db:"new_password"` // new_password 如果存在新密码 } -// UserAddAddressHandler 用户登录请求结构 -type RequestUserLogin { - Email string `json:"email"` - Password string `json:"password"` -} - // RequestAddAddress 增加地址结构 type RequestAddAddress { Id int64 `json:"id"` // address_id 地址id @@ -204,15 +173,7 @@ type RequestOrderId { RefundReason string `json:"refund_reason"` //取消原因 } -// UserLoginHandler 用户登录请求结构 -type DataUserLogin { - Token string `json:"token"` // 登录jwt token -} -// DataGuest 游客获取toekn请求结构 -type DataGuest { - Token string `json:"token"` // 登录jwt token -} // UserBasicInfoHandler 返回data结构 type DataUserBasicInfo { diff --git a/utils/auth/user.go b/utils/auth/user.go index 3acb8249..9d0e5a89 100644 --- a/utils/auth/user.go +++ b/utils/auth/user.go @@ -68,6 +68,11 @@ type BackendUserInfo struct { DepartmentId int64 `json:"department_id"` } +type OAuthInfo struct { + Id int64 `json:"id"` + Platform string `json:"platform"` +} + // 获取登录信息 func GetUserInfoFormMapClaims(claims jwt.MapClaims) (*UserInfo, error) { userinfo := &UserInfo{} @@ -195,3 +200,79 @@ func CheckValueRange[T comparable](v T, rangevalues ...T) bool { } return false } + +// GenerateRegisterToken 网站注册 token生成 +func GenerateRegisterToken(accessSecret *string, accessExpire, nowSec int64, id int64, platform string) (string, error) { + claims := make(jwt.MapClaims) + claims["exp"] = nowSec + accessExpire + claims["iat"] = nowSec + + if id == 0 { + err := errors.New("userid and guestid cannot be 0 at the same time") + logx.Error(err) + return "", err + + } + claims["id"] = id + claims["platform"] = platform + + token := jwt.New(jwt.SigningMethodHS256) + token.Claims = claims + return token.SignedString([]byte(*accessSecret)) +} + +// GetRegisterFormMapClaims 获取注册唯一token标识登录信息 +func GetRegisterFormMapClaims(claims jwt.MapClaims) (*OAuthInfo, error) { + oauthinfo := &OAuthInfo{} + if userid, ok := claims["id"]; ok { + uid, ok := userid.(float64) + if !ok { + err := errors.New(fmt.Sprint("parse uid form context err:", userid)) + logx.Error("parse uid form context err:", err) + return nil, err + } + oauthinfo.Id = int64(uid) + } else { + err := errors.New(`id not in claims`) + logx.Error(`id not in claims`) + return nil, err + } + + if splatform, ok := claims["id"]; ok { + platform, ok := splatform.(string) + if !ok { + err := errors.New(fmt.Sprint("parse uid form context err:", platform)) + logx.Error("parse uid form context err:", err) + return nil, err + } + oauthinfo.Platform = platform + } else { + err := errors.New(`id not in claims`) + logx.Error(`id not in claims`) + return nil, err + } + + return oauthinfo, nil +} + +func getRegisterJwtClaims(Token string, AccessSecret *string) (jwt.MapClaims, error) { + + token, err := jwt.Parse(Token, 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(*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)) +} diff --git a/utils/basic/basic.go b/utils/basic/basic.go index 7ec3d02f..6a3c8349 100644 --- a/utils/basic/basic.go +++ b/utils/basic/basic.go @@ -39,7 +39,9 @@ var ( CodeServiceErr = &StatusResponse{510, "server logic error"} // 服务逻辑错误 CodeUnAuth = &StatusResponse{401, "unauthorized"} // 未授权 - CodeOAuthGoogleApiErr = &StatusResponse{5070, "oauth2 google api error"} + CodeOAuthGoogleApiErr = &StatusResponse{5070, "oauth2 google api error"} + CodeOAuthRegisterTokenErr = &StatusResponse{5071, "oauth2 jwt token error"} + CodeOAuthEmailErr = &StatusResponse{5071, "Invalid email format"} CodeS3PutObjectRequestErr = &StatusResponse{5060, "s3 PutObjectRequest error"} // s3 PutObjectRequest 错误 CodeS3PutSizeLimitErr = &StatusResponse{5061, "s3 over limit size error"} // s3 超过文件大小限制 错误