女主宣言
今天小编为大家分享基于DDD的golang实现,DDD即领域驱动设计,该模式也算是比较热门的话题了。希望通过本篇文章,大家能够掌握DDD模式,能对大家有所帮助。
PS:丰富的一线技术、多元化的表现形式,尽在“360云计算”,点关注哦!
领域驱动设计模式算是比较热门的话题了。
领域驱动设计(DDD)是一种软件开发方法,通过将实现与不断演变的模型相连接,简化了开发人员面临的复杂性。
本文不会重点去解释Golang中实现DDD的相关理念,而是作者根据自己的研究对DDD的理解。
什么是DDD?
以下是考虑使用DDD的原因:
-
提供解决困难问题的原则和模式
-
将复杂的设计基于领域模型
-
在技术和领域专家之间发起创造性的协作,以迭代地完善解决领域问题的概念模型。
DDD包含4个层:
-
Domain:这是定义应用程序的域和业务逻辑的地方
-
Infrastructure:此层包含独立于我们的应用程序而存在的所有内容:外部库,数据库引擎等。
-
Application:该层用作域和界面层之间的通道。将请求从接口层发送到域层,由域层处理请求并返回响应。
-
Interface:该层包含与其他系统交互的所有内容,例如Web服务,RMI接口或Web应用程序以及批处理前端。
1
开始
我们将构建一个食物推荐API。
首先要做的是初始化依赖关系管理。我们将使用go.mod。在根目录(路径:food-app /)中,初始化go.mod:
1 | go mod init food-app |
项目的组织结构:
在该应用中,我们将使用postgres和redis数据库持久化数据。先定义一个含有连接信息的.env文件。
.env文件内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#Postgres APP_ENV=local API_PORT=8888 DB_HOST=127.0.0.1 DB_DRIVER=postgres ACCESS_SECRET=98hbun98h REFRESH_SECRET=786dfdbjhsb DB_USER=steven DB_PASSWORD=password DB_NAME=food-app DB_PORT=5432 #Mysql #DB_HOST=127.0.0.1 #DB_DRIVER=mysql #DB_USER=steven #DB_PASSWORD=here #DB_NAME=food-app #DB_PORT=3306 #Postgres Test DB TEST_DB_DRIVER=postgres TEST_DB_HOST=127.0.0.1 TEST_DB_PASSWORD=password TEST_DB_USER=steven TEST_DB_NAME=food-app-test TEST_DB_PORT=5432 #Redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD= |
该文件应位于根目录中(路径:food-app /)
2
Domain 层
我们将首先考虑领域。
该域具有几种模式。其中一些是:实体,值,存储库,服务等。
由于我们在此处构建的应用比较简单,因此我们仅考虑两种域模式:实体和存储库。
实体
这是我们定义“Schema”的地方。
例如,我们可以定义用户的结构。将该实体视为域的蓝图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
package entity import ( "food-app/infrastructure/security" "github.com/badoux/checkmail" "html" "strings" "time" ) type User struct { ID uint64 `gorm:"primary_key;auto_increment" json:"id"` FirstName string `gorm:"size:100;not null;" json:"first_name"` <iframe data-google-container-id="a!8" data-google-query-id="CNzdg-7Jyf0CFUh3YAodutoJYA" data-load-complete="true" frameborder="0" height="0" id="aswift_7" marginheight="0" marginwidth="0" name="aswift_7" scrolling="no" src="https://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-1776224780566592&output=html&h=280&adk=1974660515&adf=3471602910&pi=t.aa~a.2871807566~i.24~rp.1&w=759&fwrn=4&fwrnh=100&lmt=1678183807&num_ads=1&rafmt=1&armr=3&sem=mc&pwprc=2168828574&ad_type=text_image&format=759x280&url=https%3A%2F%2Fwww.codenong.com%2Fcs106393739%2F&fwr=0&pra=3&rh=190&rw=759&rpe=1&resp_fmts=3&wgl=1&fa=27&uach=WyJXaW5kb3dzIiwiMTAuMC4wIiwieDg2IiwiIiwiMTEwLjAuNTQ4MS4xNzgiLFtdLGZhbHNlLG51bGwsIjY0IixbWyJDaHJvbWl1bSIsIjExMC4wLjU0ODEuMTc4Il0sWyJOb3QgQShCcmFuZCIsIjI0LjAuMC4wIl0sWyJHb29nbGUgQ2hyb21lIiwiMTEwLjAuNTQ4MS4xNzgiXV0sZmFsc2Vd&dt=1678183731354&bpp=5&bdt=36212&idt=5&shv=r20230302&mjsv=m202302280101&ptt=9&saldr=aa&abxe=1&cookie=ID%3D2bdf99e63f07b66e-224a384a45d90002%3AT%3D1673698061%3ART%3D1673698061%3AS%3DALNI_MbwJvQyG1sryNJwBud3kK5snBX0KA&gpic=UID%3D00000ba423913de7%3AT%3D1673698061%3ART%3D1678183731%3AS%3DALNI_MaA7v5cjD2Om8hdosHpiYkUWb1hJw&prev_fmts=0x0%2C748x280%2C748x187%2C748x280%2C257x600%2C257x600&nras=2&correlator=401643008406&rume=1&frm=20&pv=1&ga_vid=610842400.1673698061&ga_sid=1678183720&ga_hid=79198192&ga_fc=1&u_tz=480&u_his=1&u_h=1440&u_w=2560&u_ah=1400&u_aw=2560&u_cd=24&u_sd=1&dmc=8&adx=792&ady=3711&biw=2543&bih=1297&scr_x=0&scr_y=0&eid=44759842%2C44773809%2C44777877%2C44759876%2C44759927%2C31072742%2C31061691%2C31061693&oid=2&pvsid=3815150298831894&tmod=1737639724&uas=0&nvt=1&ref=https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DoyBEXlKRSHCzclTRidyDciVC-sb-b8eFgCpYczGrb2yeTgj1YaQnCGljds_XK7Jj%26wd%3D%26eqid%3D9961a2330000936f0000000664070cf6&fc=1408&brdim=0%2C0%2C0%2C0%2C2560%2C0%2C0%2C0%2C2560%2C1297&vis=1&rsz=%7C%7Cs%7C&abl=NS&fu=128&bc=31&ifi=8&uci=a!8&btvi=1&fsb=1&xpc=EggNH3aOud&p=https%3A//www.codenong.com&dtd=76006" width="759"></iframe> LastName string `gorm:"size:100;not null;" json:"last_name"` <iframe data-google-container-id="a!9" data-google-query-id="CPneg-7Jyf0CFQwIYAodyZ0LMw" data-load-complete="true" frameborder="0" height="0" id="aswift_8" marginheight="0" marginwidth="0" name="aswift_8" scrolling="no" src="https://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-1776224780566592&output=html&h=280&adk=1974660515&adf=2430145915&pi=t.aa~a.2871807566~i.26~rp.1&w=759&fwrn=4&fwrnh=100&lmt=1678183807&num_ads=1&rafmt=1&armr=3&sem=mc&pwprc=2168828574&ad_type=text_image&format=759x280&url=https%3A%2F%2Fwww.codenong.com%2Fcs106393739%2F&fwr=0&pra=3&rh=190&rw=759&rpe=1&resp_fmts=3&wgl=1&fa=27&uach=WyJXaW5kb3dzIiwiMTAuMC4wIiwieDg2IiwiIiwiMTEwLjAuNTQ4MS4xNzgiLFtdLGZhbHNlLG51bGwsIjY0IixbWyJDaHJvbWl1bSIsIjExMC4wLjU0ODEuMTc4Il0sWyJOb3QgQShCcmFuZCIsIjI0LjAuMC4wIl0sWyJHb29nbGUgQ2hyb21lIiwiMTEwLjAuNTQ4MS4xNzgiXV0sZmFsc2Vd&dt=1678183731370&bpp=4&bdt=36227&idt=4&shv=r20230302&mjsv=m202302280101&ptt=9&saldr=aa&abxe=1&cookie=ID%3D2bdf99e63f07b66e-224a384a45d90002%3AT%3D1673698061%3ART%3D1673698061%3AS%3DALNI_MbwJvQyG1sryNJwBud3kK5snBX0KA&gpic=UID%3D00000ba423913de7%3AT%3D1673698061%3ART%3D1678183731%3AS%3DALNI_MaA7v5cjD2Om8hdosHpiYkUWb1hJw&prev_fmts=0x0%2C748x280%2C748x187%2C748x280%2C257x600%2C257x600%2C759x280&nras=3&correlator=401643008406&rume=1&frm=20&pv=1&ga_vid=610842400.1673698061&ga_sid=1678183720&ga_hid=79198192&ga_fc=1&u_tz=480&u_his=1&u_h=1440&u_w=2560&u_ah=1400&u_aw=2560&u_cd=24&u_sd=1&dmc=8&adx=792&ady=4029&biw=2543&bih=1297&scr_x=0&scr_y=0&eid=44759842%2C44773809%2C44777877%2C44759876%2C44759927%2C31072742%2C31061691%2C31061693&oid=2&pvsid=3815150298831894&tmod=1737639724&uas=0&nvt=1&ref=https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DoyBEXlKRSHCzclTRidyDciVC-sb-b8eFgCpYczGrb2yeTgj1YaQnCGljds_XK7Jj%26wd%3D%26eqid%3D9961a2330000936f0000000664070cf6&fc=1408&brdim=0%2C0%2C0%2C0%2C2560%2C0%2C0%2C0%2C2560%2C1297&vis=1&rsz=%7C%7Cs%7C&abl=NS&fu=128&bc=31&ifi=9&uci=a!9&btvi=2&fsb=1&xpc=T6AkBTnIft&p=https%3A//www.codenong.com&dtd=76001" width="759"></iframe> Email string `gorm:"size:100;not null;unique" json:"email"` Password string `gorm:"size:100;not null;" json:"password"` CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` DeletedAt *time.Time `json:"deleted_at,omitempty"` } type PublicUser struct { ID uint64 `gorm:"primary_key;auto_increment" json:"id"` FirstName string `gorm:"size:100;not null;" json:"first_name"` LastName string `gorm:"size:100;not null;" json:"last_name"` } //BeforeSave is a gorm hook func (u *User) BeforeSave() error { hashPassword, err := security.Hash(u.Password) if err != nil { return err } u.Password = string(hashPassword) return nil } type Users []User //So that we dont expose the user's email address and password to the world func (users Users) PublicUsers() []interface{} { result := make([]interface{}, len(users)) for index, user := range users { result[index] = user.PublicUser() } return result } //So that we dont expose the user's email address and password to the world func (u *User) PublicUser() interface{} { return &PublicUser{ ID: u.ID, FirstName: u.FirstName, LastName: u.LastName, } } func (u *User) Prepare() { u.FirstName = html.EscapeString(strings.TrimSpace(u.FirstName)) u.LastName = html.EscapeString(strings.TrimSpace(u.LastName)) <iframe data-google-container-id="a!a" data-google-query-id="CL7okO7Jyf0CFYcRKgodS4QLYQ" data-load-complete="true" frameborder="0" height="0" id="aswift_9" marginheight="0" marginwidth="0" name="aswift_9" scrolling="no" src="https://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-1776224780566592&output=html&h=280&adk=1974660515&adf=3367294412&pi=t.aa~a.2871807566~i.118~rp.1&w=759&fwrn=4&fwrnh=100&lmt=1678183807&num_ads=1&rafmt=1&armr=3&sem=mc&pwprc=2168828574&ad_type=text_image&format=759x280&url=https%3A%2F%2Fwww.codenong.com%2Fcs106393739%2F&fwr=0&pra=3&rh=190&rw=759&rpe=1&resp_fmts=3&wgl=1&fa=27&uach=WyJXaW5kb3dzIiwiMTAuMC4wIiwieDg2IiwiIiwiMTEwLjAuNTQ4MS4xNzgiLFtdLGZhbHNlLG51bGwsIjY0IixbWyJDaHJvbWl1bSIsIjExMC4wLjU0ODEuMTc4Il0sWyJOb3QgQShCcmFuZCIsIjI0LjAuMC4wIl0sWyJHb29nbGUgQ2hyb21lIiwiMTEwLjAuNTQ4MS4xNzgiXV0sZmFsc2Vd&dt=1678183731384&bpp=3&bdt=36242&idt=3&shv=r20230302&mjsv=m202302280101&ptt=9&saldr=aa&abxe=1&cookie=ID%3D2bdf99e63f07b66e-224a384a45d90002%3AT%3D1673698061%3ART%3D1673698061%3AS%3DALNI_MbwJvQyG1sryNJwBud3kK5snBX0KA&gpic=UID%3D00000ba423913de7%3AT%3D1673698061%3ART%3D1678183731%3AS%3DALNI_MaA7v5cjD2Om8hdosHpiYkUWb1hJw&prev_fmts=0x0%2C748x280%2C748x187%2C748x280%2C257x600%2C257x600%2C759x280%2C759x280%2C728x90&nras=5&correlator=401643008406&rume=1&frm=20&pv=1&ga_vid=610842400.1673698061&ga_sid=1678183720&ga_hid=79198192&ga_fc=1&u_tz=480&u_his=1&u_h=1440&u_w=2560&u_ah=1400&u_aw=2560&u_cd=24&u_sd=1&dmc=8&adx=792&ady=4922&biw=2543&bih=1297&scr_x=0&scr_y=0&eid=44759842%2C44773809%2C44777877%2C44759876%2C44759927%2C31072742%2C31061691%2C31061693&oid=2&pvsid=3815150298831894&tmod=1737639724&uas=0&nvt=1&ref=https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DoyBEXlKRSHCzclTRidyDciVC-sb-b8eFgCpYczGrb2yeTgj1YaQnCGljds_XK7Jj%26wd%3D%26eqid%3D9961a2330000936f0000000664070cf6&fc=1408&brdim=0%2C0%2C0%2C0%2C2560%2C0%2C2560%2C1400%2C2560%2C1297&vis=1&rsz=%7C%7Cs%7C&abl=NS&fu=128&bc=31&ifi=10&uci=a!a&btvi=4&fsb=1&xpc=aUXD412nKL&p=https%3A//www.codenong.com&dtd=76210" width="759"></iframe> u.Email = html.EscapeString(strings.TrimSpace(u.Email)) u.CreatedAt = time.Now() u.UpdatedAt = time.Now() } func (u *User) Validate(action string) map[string]string { var errorMessages = make(map[string]string) var err error switch strings.ToLower(action) { case "update": if u.Email == "" { errorMessages["email_required"] = "email required" } if u.Email != "" { if err = checkmail.ValidateFormat(u.Email); err != nil { errorMessages["invalid_email"] = "email email" } } case "login": if u.Password == "" { errorMessages["password_required"] = "password is required" } if u.Email == "" { errorMessages["email_required"] = "email is required" } if u.Email != "" { if err = checkmail.ValidateFormat(u.Email); err != nil { errorMessages["invalid_email"] = "please provide a valid email" } } case "forgotpassword": if u.Email == "" { errorMessages["email_required"] = "email required" } if u.Email != "" { if err = checkmail.ValidateFormat(u.Email); err != nil { errorMessages["invalid_email"] = "please provide a valid email" } } default: if u.FirstName == "" { errorMessages["firstname_required"] = "first name is required" } if u.LastName == "" { errorMessages["lastname_required"] = "last name is required" } if u.Password == "" { errorMessages["password_required"] = "password is required" } if u.Password != "" && len(u.Password) < 6 { errorMessages["invalid_password"] = "password should be at least 6 characters" } if u.Email == "" { errorMessages["email_required"] = "email is required" } if u.Email != "" { if err = checkmail.ValidateFormat(u.Email); err != nil { errorMessages["invalid_email"] = "please provide a valid email" } } } return errorMessages } |
在上面的文件中,定义了包含用户信息的用户结构,我们还添加了帮助程序功能,这些功能将验证和清理输入。调用了一种哈希方法,该方法用于哈希密码。这是在基础结构层中定义的。
定义 food 实体时采用相同的方法。
存储库
存储库定义了基础结构实现的方法的集合。这描绘了与给定数据库或第三方API交互的方法数量。
user 存储库如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 |
package repository import ( "food-app/domain/entity" ) type UserRepository interface { SaveUser(*entity.User) (*entity.User, map[string]string) GetUser(uint64) (*entity.User, error) GetUsers() ([]entity.User, error) GetUserByEmailAndPassword(*entity.User) (*entity.User, map[string]string) } |
方法在接口中定义。这些方法稍后将在基础结构层中实现。
food 库几乎相同。
3
Infrastructure 层
该层实现存储库中定义的方法。这些方法与数据库或第三方API交互。本文中仅考虑数据库交互。
我们可以看到 user 存储库实现如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
package persistence import ( "errors" "food-app/domain/entity" "food-app/domain/repository" "food-app/infrastructure/security" "github.com/jinzhu/gorm" "golang.org/x/crypto/bcrypt" "strings" ) type UserRepo struct { db *gorm.DB } func NewUserRepository(db *gorm.DB) *UserRepo { return &UserRepo{db} } //UserRepo implements the repository.UserRepository interface var _ repository.UserRepository = &UserRepo{} func (r *UserRepo) SaveUser(user *entity.User) (*entity.User, map[string]string) { dbErr := map[string]string{} err := r.db.Debug().Create(&user).Error if err != nil { //If the email is already taken if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "Duplicate") { dbErr["email_taken"] = "email already taken" return nil, dbErr } //any other db error dbErr["db_error"] = "database error" return nil, dbErr } return user, nil } func (r *UserRepo) GetUser(id uint64) (*entity.User, error) { var user entity.User err := r.db.Debug().Where("id = ?", id).Take(&user).Error if err != nil { return nil, err } if gorm.IsRecordNotFoundError(err) { return nil, errors.New("user not found") } return &user, nil } func (r *UserRepo) GetUsers() ([]entity.User, error) { var users []entity.User err := r.db.Debug().Find(&users).Error if err != nil { return nil, err } if gorm.IsRecordNotFoundError(err) { return nil, errors.New("user not found") } return users, nil } func (r *UserRepo) GetUserByEmailAndPassword(u *entity.User) (*entity.User, map[string]string) { var user entity.User dbErr := map[string]string{} err := r.db.Debug().Where("email = ?", u.Email).Take(&user).Error if gorm.IsRecordNotFoundError(err) { dbErr["no_user"] = "user not found" return nil, dbErr } if err != nil { dbErr["db_error"] = "database error" return nil, dbErr } //Verify the password err = security.VerifyPassword(user.Password, u.Password) if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { dbErr["incorrect_password"] = "incorrect password" return nil, dbErr } return &user, nil } |
可以看到我们实现了存储库中定义的方法。使用实现了UserRepository接口的UserRepo结构可以做到这一点,如下行所示:
1 2 |
//UserRepo implements the repository.UserRepository interface var _ repository.UserRepository = &UserRepo{} |
因此,我们通过创建包含以下内容的db.go文件来配置数据库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
package persistence import ( "fmt" "food-app/domain/entity" "food-app/domain/repository" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" ) type Repositories struct { User repository.UserRepository Food repository.FoodRepository db *gorm.DB } func NewRepositories(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) (*Repositories, error) { DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword) db, err := gorm.Open(Dbdriver, DBURL) if err != nil { return nil, err } db.LogMode(true) return &Repositories{ User: NewUserRepository(db), Food: NewFoodRepository(db), db: db, }, nil } //closes the database connection func (s *Repositories) Close() error { return s.db.Close() } //This migrate all tables func (s *Repositories) Automigrate() error { return s.db.AutoMigrate(&entity.User{}, &entity.Food{}).Error } |
在上面的文件中,我们定义了Repositories结构,该结构保存了应用中的所有存储库。我们有 user 和 food 库。该存储库还具有一个db实例,该实例被传递给 user 和 food(即NewUserRepository和NewFoodRepository)的“constructors”。
4
Application 层
我们已经在域中定义了API业务逻辑。该层连接 domain 和 interfaces 层。
以下是 user 的应用层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package application import ( "food-app/domain/entity" "food-app/domain/repository" ) type userApp struct { us repository.UserRepository } //UserApp implements the UserAppInterface var _ UserAppInterface = &userApp{} type UserAppInterface interface { SaveUser(*entity.User) (*entity.User, map[string]string) GetUsers() ([]entity.User, error) GetUser(uint64) (*entity.User, error) GetUserByEmailAndPassword(*entity.User) (*entity.User, map[string]string) } func (u *userApp) SaveUser(user *entity.User) (*entity.User, map[string]string) { return u.us.SaveUser(user) } func (u *userApp) GetUser(userId uint64) (*entity.User, error) { return u.us.GetUser(userId) } func (u *userApp) GetUsers() ([]entity.User, error) { return u.us.GetUsers() } func (u *userApp) GetUserByEmailAndPassword(user *entity.User) (*entity.User, map[string]string) { return u.us.GetUserByEmailAndPassword(user) } |
上面有保存和检索用户数据的方法。UserApp结构具有UserRepository接口,从而可以调用用户存储库方法。
5
Interfaces 层
接口是处理HTTP请求和响应的层。这里我们收到身份验证,与用户相关的内容和与食品相关的内容的传入请求。
用户处理
我们定义了保存用户,获取所有用户和获取特定用户的方法。这些可以在user_handler.go文件中找到。
观察到返回用户时,我们仅返回一个公共用户(在实体中定义)。公共用户没有敏感的用户详细信息,例如电子邮件和密码。
授权处理
login_handler负责登录,注销和刷新令牌方法。在各自文件中定义的某些方法在此文件中被调用。最好在它们的文件路径之后在存储库中检出它们。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
package interfaces import ( "fmt" "food-app/application" "food-app/domain/entity" "food-app/infrastructure/auth" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" "net/http" "os" "strconv" ) type Authenticate struct { us application.UserAppInterface rd auth.AuthInterface tk auth.TokenInterface } //Authenticate constructor func NewAuthenticate(uApp application.UserAppInterface, rd auth.AuthInterface, tk auth.TokenInterface) *Authenticate { return &Authenticate{ us: uApp, rd: rd, tk: tk, } } func (au *Authenticate) Login(c *gin.Context) { var user *entity.User var tokenErr = map[string]string{} if err := c.ShouldBindJSON(&user); err != nil { c.JSON(http.StatusUnprocessableEntity, "Invalid json provided") return } //validate request: validateUser := user.Validate("login") if len(validateUser) > 0 { c.JSON(http.StatusUnprocessableEntity, validateUser) return } u, userErr := au.us.GetUserByEmailAndPassword(user) if userErr != nil { c.JSON(http.StatusInternalServerError, userErr) return } ts, tErr := au.tk.CreateToken(u.ID) if tErr != nil { tokenErr["token_error"] = tErr.Error() c.JSON(http.StatusUnprocessableEntity, tErr.Error()) return } saveErr := au.rd.CreateAuth(u.ID, ts) if saveErr != nil { c.JSON(http.StatusInternalServerError, saveErr.Error()) return } userData := make(map[string]interface{}) userData["access_token"] = ts.AccessToken userData["refresh_token"] = ts.RefreshToken userData["id"] = u.ID userData["first_name"] = u.FirstName userData["last_name"] = u.LastName c.JSON(http.StatusOK, userData) } func (au *Authenticate) Logout(c *gin.Context) { //check is the user is authenticated first metadata, err := au.tk.ExtractTokenMetadata(c.Request) if err != nil { c.JSON(http.StatusUnauthorized, "Unauthorized") return } //if the access token exist and it is still valid, then delete both the access token and the refresh token deleteErr := au.rd.DeleteTokens(metadata) if deleteErr != nil { c.JSON(http.StatusUnauthorized, deleteErr.Error()) return } c.JSON(http.StatusOK, "Successfully logged out") } //Refresh is the function that uses the refresh_token to generate new pairs of refresh and access tokens. func (au *Authenticate) Refresh(c *gin.Context) { mapToken := map[string]string{} if err := c.ShouldBindJSON(&mapToken); err != nil { c.JSON(http.StatusUnprocessableEntity, err.Error()) return } refreshToken := mapToken["refresh_token"] //verify the token token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { //Make sure that the token method conform to "SigningMethodHMAC" <iframe data-google-container-id="a!b" data-google-query-id="CMi9lPbJyf0CFUhwYAodMV8NFw" data-load-complete="true" frameborder="0" height="0" id="aswift_10" marginheight="0" marginwidth="0" name="aswift_10" scrolling="no" src="https://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-1776224780566592&output=html&h=280&adk=2969179070&adf=2341695197&pi=t.aa~a.299378223~i.192~rp.1&w=995&fwrn=4&fwrnh=100&lmt=1678183824&num_ads=1&rafmt=1&armr=3&sem=mc&pwprc=2168828574&ad_type=text_image&format=995x280&url=https%3A%2F%2Fwww.codenong.com%2Fcs106393739%2F&fwr=0&pra=3&rh=200&rw=995&rpe=1&resp_fmts=3&wgl=1&fa=27&uach=WyJXaW5kb3dzIiwiMTAuMC4wIiwieDg2IiwiIiwiMTEwLjAuNTQ4MS4xNzgiLFtdLGZhbHNlLG51bGwsIjY0IixbWyJDaHJvbWl1bSIsIjExMC4wLjU0ODEuMTc4Il0sWyJOb3QgQShCcmFuZCIsIjI0LjAuMC4wIl0sWyJHb29nbGUgQ2hyb21lIiwiMTEwLjAuNTQ4MS4xNzgiXV0sZmFsc2Vd&dt=1678183731398&bpp=3&bdt=36255&idt=3&shv=r20230302&mjsv=m202302280101&ptt=9&saldr=aa&abxe=1&cookie=ID%3D2bdf99e63f07b66e-224a384a45d90002%3AT%3D1673698061%3ART%3D1673698061%3AS%3DALNI_MbwJvQyG1sryNJwBud3kK5snBX0KA&gpic=UID%3D00000ba423913de7%3AT%3D1673698061%3ART%3D1678183731%3AS%3DALNI_MaA7v5cjD2Om8hdosHpiYkUWb1hJw&prev_fmts=0x0%2C748x280%2C748x187%2C748x280%2C257x600%2C257x600%2C759x280%2C759x280%2C728x90%2C759x280&nras=6&correlator=401643008406&rume=1&frm=20&pv=1&ga_vid=610842400.1673698061&ga_sid=1678183720&ga_hid=79198192&ga_fc=1&u_tz=480&u_his=1&u_h=1440&u_w=2560&u_ah=1400&u_aw=2560&u_cd=24&u_sd=1&dmc=8&adx=792&ady=14641&biw=2543&bih=1297&scr_x=0&scr_y=9656&eid=44759842%2C44773809%2C44777877%2C44759876%2C44759927%2C31072742%2C31061691%2C31061693&oid=2&pvsid=3815150298831894&tmod=1737639724&uas=0&nvt=1&ref=https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DoyBEXlKRSHCzclTRidyDciVC-sb-b8eFgCpYczGrb2yeTgj1YaQnCGljds_XK7Jj%26wd%3D%26eqid%3D9961a2330000936f0000000664070cf6&fc=1408&brdim=0%2C0%2C0%2C0%2C2560%2C0%2C2560%2C1400%2C2560%2C1297&vis=1&rsz=%7C%7Cs%7C&abl=NS&fu=128&bc=31&ifi=11&uci=a!b&btvi=5&fsb=1&xpc=M9HhWI76Z3&p=https%3A//www.codenong.com&dtd=93026" width="995"></iframe> if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(os.Getenv("REFRESH_SECRET")), nil }) //any error may be due to token expiration if err != nil { c.JSON(http.StatusUnauthorized, err.Error()) return } //is token valid? if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid { c.JSON(http.StatusUnauthorized, err) return } //Since token is valid, get the uuid: claims, ok := token.Claims.(jwt.MapClaims) if ok && token.Valid { refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string if !ok { c.JSON(http.StatusUnprocessableEntity, "Cannot get uuid") return } userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64) if err != nil { c.JSON(http.StatusUnprocessableEntity, "Error occurred") return } //Delete the previous Refresh Token delErr := au.rd.DeleteRefresh(refreshUuid) if delErr != nil { //if any goes wrong c.JSON(http.StatusUnauthorized, "unauthorized") return } //Create new pairs of refresh and access tokens ts, createErr := au.tk.CreateToken(userId) if createErr != nil { c.JSON(http.StatusForbidden, createErr.Error()) return } //save the tokens metadata to redis saveErr := au.rd.CreateAuth(userId, ts) if saveErr != nil { c.JSON(http.StatusForbidden, saveErr.Error()) return } tokens := map[string]string{ "access_token": ts.AccessToken, "refresh_token": ts.RefreshToken, } c.JSON(http.StatusCreated, tokens) } else { c.JSON(http.StatusUnauthorized, "refresh token expired") } } |
6
运行程序
我们测试一下该应用。我们将连接路由,连接到数据库并启动应用程序。
在根目录中定义的main.go文件中完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
package main import ( "food-app/infrastructure/auth" "food-app/infrastructure/persistence" "food-app/interfaces" "food-app/interfaces/fileupload" "food-app/interfaces/middleware" "github.com/gin-gonic/gin" "github.com/joho/godotenv" "log" "os" ) func init() { //To load our environmental variables. if err := godotenv.Load(); err != nil { log.Println("no env gotten") } } func main() { dbdriver := os.Getenv("DB_DRIVER") host := os.Getenv("DB_HOST") password := os.Getenv("DB_PASSWORD") user := os.Getenv("DB_USER") dbname := os.Getenv("DB_NAME") port := os.Getenv("DB_PORT") //redis details redis_host := os.Getenv("REDIS_HOST") redis_port := os.Getenv("REDIS_PORT") redis_password := os.Getenv("REDIS_PASSWORD") services, err := persistence.NewRepositories(dbdriver, user, password, port, host, dbname) if err != nil { panic(err) } defer services.Close() services.Automigrate() redisService, err := auth.NewRedisDB(redis_host, redis_port, redis_password) if err != nil { log.Fatal(err) } tk := auth.NewToken() fd := fileupload.NewFileUpload() users := interfaces.NewUsers(services.User, redisService.Auth, tk) foods := interfaces.NewFood(services.Food, services.User, fd, redisService.Auth, tk) <iframe data-google-container-id="a!c" data-google-query-id="CJKVq_bJyf0CFSTOTAId8ccEjQ" data-load-complete="true" frameborder="0" height="0" id="aswift_11" marginheight="0" marginwidth="0" name="aswift_11" scrolling="no" src="https://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-1776224780566592&output=html&h=280&adk=3090783630&adf=3100859383&pi=t.aa~a.4045601092~i.104~rp.1&w=928&fwrn=4&fwrnh=100&lmt=1678183824&num_ads=1&rafmt=1&armr=3&sem=mc&pwprc=2168828574&ad_type=text_image&format=928x280&url=https%3A%2F%2Fwww.codenong.com%2Fcs106393739%2F&fwr=0&pra=3&rh=200&rw=927&rpe=1&resp_fmts=3&wgl=1&fa=27&uach=WyJXaW5kb3dzIiwiMTAuMC4wIiwieDg2IiwiIiwiMTEwLjAuNTQ4MS4xNzgiLFtdLGZhbHNlLG51bGwsIjY0IixbWyJDaHJvbWl1bSIsIjExMC4wLjU0ODEuMTc4Il0sWyJOb3QgQShCcmFuZCIsIjI0LjAuMC4wIl0sWyJHb29nbGUgQ2hyb21lIiwiMTEwLjAuNTQ4MS4xNzgiXV0sZmFsc2Vd&dt=1678183731411&bpp=2&bdt=36269&idt=2&shv=r20230302&mjsv=m202302280101&ptt=9&saldr=aa&abxe=1&cookie=ID%3D2bdf99e63f07b66e-224a384a45d90002%3AT%3D1673698061%3ART%3D1673698061%3AS%3DALNI_MbwJvQyG1sryNJwBud3kK5snBX0KA&gpic=UID%3D00000ba423913de7%3AT%3D1673698061%3ART%3D1678183731%3AS%3DALNI_MaA7v5cjD2Om8hdosHpiYkUWb1hJw&prev_fmts=0x0%2C748x280%2C748x187%2C748x280%2C257x600%2C257x600%2C759x280%2C759x280%2C728x90%2C759x280%2C995x280&nras=7&correlator=401643008406&rume=1&frm=20&pv=1&ga_vid=610842400.1673698061&ga_sid=1678183720&ga_hid=79198192&ga_fc=1&u_tz=480&u_his=1&u_h=1440&u_w=2560&u_ah=1400&u_aw=2560&u_cd=24&u_sd=1&dmc=8&adx=790&ady=16901&biw=2543&bih=1297&scr_x=0&scr_y=11780&eid=44759842%2C44773809%2C44777877%2C44759876%2C44759927%2C31072742%2C31061691%2C31061693&oid=2&pvsid=3815150298831894&tmod=1737639724&uas=0&nvt=1&ref=https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DoyBEXlKRSHCzclTRidyDciVC-sb-b8eFgCpYczGrb2yeTgj1YaQnCGljds_XK7Jj%26wd%3D%26eqid%3D9961a2330000936f0000000664070cf6&fc=1408&brdim=0%2C0%2C0%2C0%2C2560%2C0%2C2560%2C1400%2C2560%2C1297&vis=1&rsz=%7C%7Cs%7C&abl=NS&cms=2&fu=128&bc=31&ifi=12&uci=a!c&btvi=6&fsb=1&xpc=znzp1b9th9&p=https%3A//www.codenong.com&dtd=93397" width="928"></iframe> authenticate := interfaces.NewAuthenticate(services.User, redisService.Auth, tk) r := gin.Default() r.Use(middleware.CORSMiddleware()) //For CORS //user routes r.POST("/users", users.SaveUser) r.GET("/users", users.GetUsers) r.GET("/users/:user_id", users.GetUser) //post routes r.POST("/food", middleware.AuthMiddleware(), middleware.MaxSizeAllowed(8192000), foods.SaveFood) r.PUT("/food/:food_id", middleware.AuthMiddleware(), middleware.MaxSizeAllowed(8192000), foods.UpdateFood) r.GET("/food/:food_id", foods.GetFoodAndCreator) r.DELETE("/food/:food_id", middleware.AuthMiddleware(), foods.DeleteFood) r.GET("/food", foods.GetAllFood) //authentication routes r.POST("/login", authenticate.Login) r.POST("/logout", authenticate.Logout) r.POST("/refresh", authenticate.Refresh) //Starting the application app_port := os.Getenv("PORT") //using heroku host if app_port == "" { app_port = "8888" //localhost } log.Fatal(r.Run(":"+app_port)) } |
其中的中间件也是定义在 interfaces 层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
package middleware import ( "bytes" "food-app/infrastructure/auth" "github.com/gin-gonic/gin" "io/ioutil" "net/http" ) func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { err := auth.TokenValid(c.Request) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "status": http.StatusUnauthorized, "error": err.Error(), }) c.Abort() return } c.Next() } } func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } } //Avoid a large file from loading into memory //If the file size is greater than 8MB dont allow it to even load into memory and waste our time. func MaxSizeAllowed(n int64) gin.HandlerFunc { return func(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, n) buff, errRead := c.GetRawData() if errRead != nil { //c.JSON(http.StatusRequestEntityTooLarge,"too large") c.JSON(http.StatusRequestEntityTooLarge, gin.H{ "status": http.StatusRequestEntityTooLarge, "upload_err": "too large: upload an image less than 8MB", }) c.Abort() return } buf := bytes.NewBuffer(buff) c.Request.Body = ioutil.NopCloser(buf) } } |
我们现在可以使用以下命令运行该应用:
1 | go run main.go |