# Ginweb02 **Repository Path**: YePitfall/ginweb02 ## Basic Information - **Project Name**: Ginweb02 - **Description**: it's my second gin project include vue+ gin+ gorm ~ - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-11-06 - **Last Updated**: 2021-12-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 1、简介 该项目是一个Gin+Vue的前后端分离的个人博客项目,这一部分主要是Gin后端内容 ## 1.2、主要实现: **用户模块:登录、注册** **Category模块:增删改查** **Post模块:增删改查** ## 1.3、其中主要的技术: **Git作为版本控制管理** **Gin框架的使用** **包括了中间件的使用,路由组的使用** **JWT与中间件在用户认证(Authentication)的运用** **CORS跨域问题的处理** **GORM框架的使用** **Viper的配置管理** # 2、包和代码概述 这部分主要描述各个包中文件的功能,附上部分代码作为理解,其中的知识点对刚入门十几天的我来说受益良多。 ## 2.1、common包 ### database.go 主要是用于初始化数据库连接,通过viper来读取配置文件 ```go func InitDB() { driverName := viper.GetString("datasource.driverName") host := viper.GetString("datasource.host") port := viper.GetString("datasource.port") database := viper.GetString("datasource.database") username := viper.GetString("datasource.username") password := viper.GetString("datasource.password") charset := viper.GetString("datasource.charset") args := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True", username, password, host, port, database, charset) db, err := gorm.Open(driverName, args) if err != nil { panic("failed to connect database err: " + err.Error()) } db.AutoMigrate(&model.User{}) DB = db } ``` ### jwt.go 这个文件主要用于生成json web token 用于用户的认证,以及jwt的解析,这部分内容较为新颖对我来说,代码中附带了详细的注释 ```go var jwtKey = []byte("a_secret_crect") //封装了 标准的jwt声明和自己额外添加的声明 UserID type Claims struct { UserId uint jwt.StandardClaims } //token的意义主要在于不需要重新输入用户名和密码,但是无法避免服务端查库 /* 一共三部分 第一部分header信息 ,第二部分的声明信息 可以base64解码,注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJVc2VySWQiOjEsImV4cCI6MTYzNjg4MzIzNSwiaWF0IjoxNjM2Mjc4NDM1LCJpc3MiOiJZYmwiLCJzdWIiOiJ1c2VyIHRva2VuIn0. a0B5sP8bs04FtKnd0OxPekek83jGrr6TghllrcrcmO0" {"alg":"HS256","typ":"JWT"} {"UserId":1,"exp":1636883235,"iat":1636278435,"iss":"Ybl","sub":"user token"} */ func ReleaseToken(user model.User) (string, error) { //设置token的有效期 7天 expirationTime := time.Now().Add(7 * 24 * time.Hour) //把要发送的信息初始化好 Claims := &Claims{ UserId: user.ID, StandardClaims: jwt.StandardClaims{ ExpiresAt: expirationTime.Unix(), IssuedAt: time.Now().Unix(), Issuer: "Ybl", //谁发放的 Subject: "user token", //发放的主题 }, } //把刚刚的内容加密使用JWT签名算法 这部分就是摘要 ,把内容+hash 生成摘要 token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims) //这里要HS256 //使用我们自己设置的密钥来生成token ,后面也用该密钥来解密 属于是对称加密,摘要用这个密钥来加密 生成签名,最后返回tokenstring tokenString, err := token.SignedString(jwtKey) if err != nil { return "", err } return tokenString, nil } func ParseToken(tokenstring string) (*jwt.Token, *Claims, error) { claims := &Claims{} //这里相当于对tokenstring的第三部分的解密,前两部分是可以直接base64解密的,第三部分是加密过的 token, err := jwt.ParseWithClaims(tokenstring, claims, func(token *jwt.Token) (interface{}, error) { return jwtKey, nil }) //返回claim return token, claims, err ``` ## 2.2、config 这个包是用来放配置文件的 ,通过viper读取 ### application.yml ```yaml server: port: 8080 datasource: driverName: mysql host: 127.0.0.1 port: 3306 database: ginessential username: root password: 109456 charset: utf8 ``` ## 2.3、controller 包含四个文件,用于控制业务逻辑 ## 2.4、dto dto的意思是data transfer obejct数据传输对象,由于User中信息太多,我们只想返回部分,所以在这里截取 ```go //DTO data transfer onject 数据传输对象 //由于User中信息太多,我们只想返回部分,所以在这里截取 type UserDto struct { Name string `json:"name"` Telephone string `json:"telephone"` } func ToUserDto(user model.User) UserDto { return UserDto{ Name: user.Name, Telephone: user.Telephone, } } ``` ## 2.5、middleware ### AuthMiddleware .go 非常不错的新知识对于我来说,通过读取http header中的“Authorization“ 来获取token,通过之前common包中的jwt来解析所获得token从而达到用户的认证。 ```go func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { //获取authorization header tokenstring := c.GetHeader("Authorization") //验证token的格式 ,如果token为空或者不是以Bearer 开头 注意这里有空格 if tokenstring == "" || !strings.HasPrefix(tokenstring, "Bearer ") { c.IndentedJSON(http.StatusUnauthorized, gin.H{ "code": 401, "msg": "权限不足", }) c.Abort() return } //如果格式正确 tokenstring = tokenstring[7:] //bearer 占了7位 截取后面的真正token //解析tokenstring token, claims, err := common.ParseToken(tokenstring) //如果有错误,或者token无效了,超时?之类的 if err != nil || !token.Valid { c.IndentedJSON(http.StatusUnauthorized, gin.H{ "code": 401, "msg": "权限不足", }) c.Abort() return } //否则就是正确 ,验证通过获取claim中的userID userID := claims.UserId db := common.GetDB() var user model.User db.First(&user, userID) //用户不存在 if user.ID == 0 { c.IndentedJSON(http.StatusUnauthorized, gin.H{ "code": 401, "msg": "权限不足", }) c.Abort() return } //用户存在,将用户的信息写入上下文 c.Set("user", user) c.Next() } } ``` ### CORSMiddleware.go 跨域问题,如果请求的发送方和接收方不是同一个端口同一个IP就会出现跨域问题 CORS,https://zhuanlan.zhihu.com/p/66484450(知识补充) 请求方在做PUT POST等一些主动的方法的时候 **会先发一个OPTIONS 方法,来判断是否能继续通过该API获得资源,如果可以在真正执行PUT POST等方法**,因此我们要设定一些返回值 ,来解决跨域问题 ```go func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { //设置跨域问题 允许8080端口访问本服务的端口,可以*号 c.Writer.Header().Set("Access-Control-Allow-Origin","http://localhost:8080") //运行访问的时间 c.Writer.Header().Set("Access-Control-Max-Age","84600") //允许使用的方法 GET POST c.Writer.Header().Set("Access-Control-Allow-Methods","*") //允许使用的Header 可以指定某一些header之类的 c.Writer.Header().Set("Access-Control-Allow-Headers","*") c.Writer.Header().Set("Access-Control-Allow-Credentials","true") //如果请求的是预检命令 (OPTIONS方法)详情见 ,https://zhuanlan.zhihu.com/p/66484450 if c.Request.Method==http.MethodOptions{ log.Println("------CORSMiddleware-----------") c.AbortWithStatus(200) }else{ c.Next() } } } ``` ### RecoverMiddleware.go 主要适用于controller中请求的api出现panic的报错信息的传递,这里让我知道了recover一定要 defer一定要搭配,而且Next()的重要性,如果没有Next其实他就不算一个真正的中间件!!! ```go func RecoverMiddleware() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err:=recover();err!=nil{ response.Failed(c,nil,fmt.Sprintf("%s",err)) } }() c.Next() //这里一定要c.NEXT() //虽然加不加都是要进去下一个函数,但是如果你不加,那么就是recoverMiddleware函数整个执行完之后进入,这时候defer也执行完毕了, //但是你加了next,你就会在recoverMiddleware之中 进去下一个函数去执行,derfer函数还在,出了paninc就可以收到 //你不加的话 整个函数已经执行完了 defer也没了,所以抓不到paninc } } ``` ## 2.6、Model 主要是User 等模型的定义 ## 2.7、repository 改包主要是用于封装一些数据库操作的函数,由于数据库操作写在controller中比较臃肿,于是我就把方法提出来,放到这里来对数据库操作。 ## 2.8、response 就是对客户端响应的封装 ## 2.9、util 一些自己用的函数 比如随机生成名字之类的。 ## 2.10、vo 这个是叫做验证器,gin 自带的validator的一些方法来验证前端发送的数据。 这个包就是用于前端发送数据,我们后端需要绑定前端发送的json数据。而这个就是用于绑定json的模型,虽然你直接用model也是可以的。绑定的时候你可以设定一些参数,比如是否必须填,长度多少 看代码就比较清楚了 ```go //前端传来的数据验证,其中的tag为 binding "required" 表示不为空 type CreateCategoryRequest struct { Name string `json:"name" binding:"required"` //gin 自带的validator ,可以去gin官网看 其他的参数 } type CreatePostRequest struct { CategoryId uint `json:"category_id" binding:"required"` Title string `json:"title" binding:"required,max=10"` //最大长度 HeadImg string `json:"head_img"` //图片的连接地址 Content string `json:"content" binding:"required" ` } ``` ## 2.11、main.go 整个函数的 启动,包括viper的预先设置,用于读取配置 ```go func main() { InitConfig() common.InitDB() //defer db.Close() r := gin.Default() r = CollectRouter(r) port:=viper.GetString("server.port") r.Run(":"+port) } func InitConfig() { workDir,_:=os.Getwd() //获取工作目录 viper.SetConfigName("application") viper.SetConfigType("yml") viper.AddConfigPath(workDir+"/config") err:=viper.ReadInConfig() if err!=nil{ panic(err) } } ``` ## 2.12 、routers.go 路由注册,包括了中间件的使用,路由组的使用 ```go func CollectRouter(r *gin.Engine) *gin.Engine { r.Use(middleware.CORSMiddleware(),middleware.RecoverMiddleware()) r.POST("/api/auth/register", controller.Register) r.POST("/api/auth/login", controller.Login) r.GET("/api/auth/info", middleware.AuthMiddleware(), controller.Info) categoryRoutes:=r.Group("/categories") catergoryController:=controller.NewCategoryController() categoryRoutes.POST("",catergoryController.Create) categoryRoutes.PUT("/:id",catergoryController.Update) categoryRoutes.GET("/:id",catergoryController.Show) categoryRoutes.DELETE("/:id",catergoryController.Delete) //PATCH 和put方法类似,只不过put是全部替换,而PATCH是修改部分 //categoryRoutes.PATCH("/:id") postRoutes:=r.Group("/posts") postRoutes.Use(middleware.AuthMiddleware()) postController:=controller.NewPostController() postRoutes.POST("",postController.Create) postRoutes.PUT("/:id",postController.Update) postRoutes.GET("/:id",postController.Show) postRoutes.DELETE("/:id",postController.Delete) postRoutes.POST("/page/list",postController.PageList) return r } ```