This commit is contained in:
eson 2023-07-24 17:22:06 +08:00
parent 7ec78d1c35
commit a10e100364
23 changed files with 1004 additions and 137 deletions

36
server/auth/auth.go Normal file
View File

@ -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()
}

19
server/auth/etc/auth.yaml Normal file
View File

@ -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"

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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),
},
},
)
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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&register_token=%s",
l.token,
l.isRegistered,
l.registerToken,
)
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<title>Redirect</title>
<script type="text/javascript">
window.onload = function() {
window.location = "%s";
}
</script>
</head>
<body>
</body>
</html>
`, 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)
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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"

View File

@ -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",

View File

@ -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()
}

View File

@ -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&register_token=%s",
l.token,
l.isRegistered,
l.registerToken,
)
if resp.Code == 200 {
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<title>Redirect</title>
<script type="text/javascript">
window.onload = function() {
window.location = "%s";
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)
}
</script>
</head>
<body>
</body>
</html>
`, rurl)
fmt.Fprintln(w, html)
l.registerToken = rtoken
}
rurl := fmt.Sprintf(
l.svcCtx.Config.MainAddress+"/oauth?token=%s&is_registered=%t&register_token=%s",
l.token,
l.isRegistered,
l.registerToken,
)
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<title>Redirect</title>
<script type="text/javascript">
window.onload = function() {
window.location = "%s";
}
</script>
</head>
<body>
</body>
</html>
`, 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)
}

View File

@ -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"` //是否默认

53
server_api/auth.api Normal file
View File

@ -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
}

View File

@ -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 {

View File

@ -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))
}

View File

@ -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 超过文件大小限制 错误