diff --git a/.gitignore b/.gitignore index 5aca732..013d3cf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,9 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out .idea -storage/* blog.iml go.sum # Dependency directories (remove the comment below to include it) # vendor/ - +storage/logs/* +storage/uploads/* \ No newline at end of file diff --git a/configs/config.yaml b/configs/config.yaml index d093da1..ecf6a6a 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -26,4 +26,8 @@ Database: Charset: utf8mb4 ParseTime: True MaxIdleConns: 10 - MaxOpenConns: 30 \ No newline at end of file + MaxOpenConns: 30 +JWT: + Secret: eddycjy + Issuer: blog-service + Expire: 7200 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 2ecc6b9..1cbe6c8 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -32,6 +32,13 @@ var doc = `{ ], "summary": "获取多个标签", "parameters": [ + { + "type": "string", + "description": "token", + "name": "token", + "in": "query", + "required": true + }, { "maxLength": 100, "type": "string", @@ -246,6 +253,62 @@ var doc = `{ } } }, + "/auth": { + "post": { + "produces": [ + "application/json" + ], + "summary": "获取token", + "parameters": [ + { + "type": "string", + "description": "appkey", + "name": "app_key", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "appsecret", + "name": "app_secret", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.res" + }, + { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + } + }, "/upload/file": { "post": { "produces": [ @@ -297,6 +360,9 @@ var doc = `{ } }, "definitions": { + "api.res": { + "type": "object" + }, "app.Pager": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index e5006c7..342133b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -15,6 +15,13 @@ ], "summary": "获取多个标签", "parameters": [ + { + "type": "string", + "description": "token", + "name": "token", + "in": "query", + "required": true + }, { "maxLength": 100, "type": "string", @@ -229,6 +236,62 @@ } } }, + "/auth": { + "post": { + "produces": [ + "application/json" + ], + "summary": "获取token", + "parameters": [ + { + "type": "string", + "description": "appkey", + "name": "app_key", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "appsecret", + "name": "app_secret", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.res" + }, + { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + } + }, "/upload/file": { "post": { "produces": [ @@ -280,6 +343,9 @@ } }, "definitions": { + "api.res": { + "type": "object" + }, "app.Pager": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b0f4456..18197e1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,6 @@ definitions: + api.res: + type: object app.Pager: properties: page: @@ -57,6 +59,11 @@ paths: /api/v1/tags: get: parameters: + - description: token + in: query + name: token + required: true + type: string - description: 标签名称 in: query maxLength: 100 @@ -205,6 +212,40 @@ paths: schema: $ref: '#/definitions/errorcode.Error' summary: 更新标签 + /auth: + post: + parameters: + - description: appkey + in: formData + name: app_key + required: true + type: string + - description: appsecret + in: formData + name: app_secret + required: true + type: string + produces: + - application/json + responses: + "200": + description: 成功 + schema: + allOf: + - $ref: '#/definitions/api.res' + - properties: + token: + type: string + type: object + "400": + description: 请求错误 + schema: + $ref: '#/definitions/errorcode.Error' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/errorcode.Error' + summary: 获取token /upload/file: post: parameters: diff --git a/global/setting.go b/global/setting.go index 2cdeb77..ad6f8ea 100644 --- a/global/setting.go +++ b/global/setting.go @@ -10,4 +10,5 @@ var ( AppSetting *setting.AppSettingS DatabaseSetting *setting.DatabaseSettingS Logger *logger.Logger + JWTSetting *setting.JWT ) diff --git a/go.mod b/go.mod index cf34a83..8811e9c 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gin-gonic/gin v1.7.2 github.com/go-openapi/spec v0.20.3 // indirect diff --git a/internal/dao/auth.go b/internal/dao/auth.go new file mode 100644 index 0000000..1444eb5 --- /dev/null +++ b/internal/dao/auth.go @@ -0,0 +1,8 @@ +package dao + +import "blog/internal/model" + +func (d *Dao) GetAuth(appKey, appSecret string) (model.Auth, error) { + auth := model.Auth{AppKey: appKey, AppSecret: appSecret} + return auth.Get(d.engine) +} diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go new file mode 100644 index 0000000..efc6414 --- /dev/null +++ b/internal/middleware/jwt.go @@ -0,0 +1,44 @@ +package middleware + +import ( + "blog/pkg/app" + "blog/pkg/errorcode" + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" +) + +func JWT() gin.HandlerFunc { + return func(c *gin.Context) { + var ( + token string + ecode = errorcode.Success + ) + if s, exist := c.GetQuery("token"); exist { + token = s + } else { + token = c.GetHeader("token") + } + if token == "" { + ecode = errorcode.InvalidParams + } else { + _, err := app.ParseToken(token) + if err != nil { + switch err.(*jwt.ValidationError).Errors { + case jwt.ValidationErrorExpired: + ecode = errorcode.UnauthorizedTokenTimeout + default: + ecode = errorcode.UnauthorizedTokenError + } + } + } + + if ecode != errorcode.Success { + response := app.NewResponse(c) + response.ToErrorResponse(ecode) + c.Abort() + return + } + + c.Next() + } +} diff --git a/internal/model/auth.go b/internal/model/auth.go new file mode 100644 index 0000000..1896bdd --- /dev/null +++ b/internal/model/auth.go @@ -0,0 +1,24 @@ +package model + +import "github.com/jinzhu/gorm" + +type Auth struct { + *Model + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` +} + +func (a Auth) TableName() string { + return "blog_auth" +} + +func (a Auth) Get(db *gorm.DB) (Auth, error) { + var auth Auth + db = db.Where("app_key = ? AND app_secret = ? AND is_del = ?", a.AppKey, a.AppSecret, 0) + err := db.First(&auth).Error + if err != nil && err != gorm.ErrRecordNotFound { + return auth, err + } + + return auth, nil +} diff --git a/internal/routess/api/auth.go b/internal/routess/api/auth.go new file mode 100644 index 0000000..fe0f0bb --- /dev/null +++ b/internal/routess/api/auth.go @@ -0,0 +1,51 @@ +package api + +import ( + "blog/global" + "blog/internal/service" + "blog/pkg/app" + "blog/pkg/errorcode" + "github.com/gin-gonic/gin" +) + +type res struct { + token string +} + +// @Summary 获取token +// @Produce json +// @Param app_key formData string true "appkey" required +// @Param app_secret formData string true "appsecret" required +// @Success 200 {object} res{token=string} "成功" +// @Failure 400 {object} errorcode.Error "请求错误" +// @Failure 500 {object} errorcode.Error "内部错误" +// @Router /auth [post] +func GetAuth(c *gin.Context) { + param := service.AuthRequest{} + response := app.NewResponse(c) + valid, errs := app.BindAndValid(c, ¶m) + if !valid { + global.Logger.Errorf("app.BindAndValid errs: %v", errs) + response.ToErrorResponse(errorcode.InvalidParams.WithDetails(errs.Errors()...)) + return + } + + svc := service.New(c.Request.Context()) + err := svc.CheckAuth(¶m) + if err != nil { + global.Logger.Errorf("svc.CheckAuth err: %v", err) + response.ToErrorResponse(errorcode.UnauthorizedAuthNotExist) + return + } + + token, err := app.GenerateToken(param.AppKey, param.AppSecret) + if err != nil { + global.Logger.Errorf("app.GenerateToken err: %v", err) + response.ToErrorResponse(errorcode.UnauthorizedTokenGenerate) + return + } + + response.ToResponse(gin.H{ + "token": token, + }) +} diff --git a/internal/routess/api/v1/tag.go b/internal/routess/api/v1/tag.go index d1bbeb5..acca23e 100644 --- a/internal/routess/api/v1/tag.go +++ b/internal/routess/api/v1/tag.go @@ -20,6 +20,7 @@ func (t Tag) Get(c *gin.Context) {} // @Summary 获取多个标签 // @Produce json +// @Param token query string true "token" // @Param name query string false "标签名称" maxlength(100) // @Param state query int false "状态" Enums(0, 1) default(1) // @Param page query int false "页码" default(1) diff --git a/internal/routess/router.go b/internal/routess/router.go index f07e9ca..22c021d 100644 --- a/internal/routess/router.go +++ b/internal/routess/router.go @@ -17,6 +17,7 @@ func NewRouter() *gin.Engine { r.Use(gin.Logger()) r.Use(gin.Recovery()) r.Use(middleware.Translations()) + article := v1.NewArticle() tag := v1.NewTag() upload := api.NewUpload() @@ -24,7 +25,9 @@ func NewRouter() *gin.Engine { r.StaticFS("/static", http.Dir(global.AppSetting.UploadSavePath)) //r.GET("/static/*any",api.ReadFile) r.GET("/doc/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + r.POST("/auth", api.GetAuth) apiv1 := r.Group("/api/v1") + apiv1.Use(middleware.JWT()) { apiv1.POST("/tags", tag.Create) apiv1.DELETE("/tags/:id", tag.Delete) diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 0000000..68fe029 --- /dev/null +++ b/internal/service/auth.go @@ -0,0 +1,21 @@ +package service + +import "errors" + +type AuthRequest struct { + AppKey string `form:"app_key" binding:"required"` + AppSecret string `form:"app_secret" binding:"required"` +} + +func (svc *Service) CheckAuth(param *AuthRequest) error { + auth, err := svc.dao.GetAuth(param.AppKey, param.AppSecret) + if err != nil { + return err + } + + if auth.ID > 0 { + return nil + } + + return errors.New("auth info does not exist") +} diff --git a/main.go b/main.go index 15d1663..711b2fc 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,12 @@ func setupSetting() error { if err != nil { return err } + err = newSetting.ReadSection("JWT", &global.JWTSetting) + if err != nil { + return err + } + global.JWTSetting.Expire *= time.Second global.ServerSetting.ReadTimeout *= time.Second global.ServerSetting.WriteTimeout *= time.Second return nil @@ -76,7 +81,6 @@ func setupSetting() error { // @termsOfService https://github.com/go-programming-tour-book func main() { gin.SetMode(global.ServerSetting.RunMode) - global.Logger.Infof("%s: go-programming-tour-book/%s", "eddycjy", "blog-service") r := routess.NewRouter() s := &http.Server{ diff --git a/pkg/app/jwt.go b/pkg/app/jwt.go new file mode 100644 index 0000000..3873425 --- /dev/null +++ b/pkg/app/jwt.go @@ -0,0 +1,51 @@ +package app + +import ( + "blog/global" + "blog/pkg/util" + "github.com/dgrijalva/jwt-go" + "time" +) + +type Claims struct { + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` + jwt.StandardClaims +} + +func GetJWTSecret() []byte { + return []byte(global.JWTSetting.Secret) +} + +func ParseToken(token string) (*Claims, error) { + tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return GetJWTSecret(), nil + }) + if err != nil { + return nil, err + } + if tokenClaims != nil { + if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { + return claims, nil + } + } + + return nil, err +} + +func GenerateToken(appKey, appSecret string) (string, error) { + nowTime := time.Now() + expireTime := nowTime.Add(global.JWTSetting.Expire) + claims := Claims{ + AppKey: util.EncodeMD5(appKey), + AppSecret: util.EncodeMD5(appSecret), + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expireTime.Unix(), + Issuer: global.JWTSetting.Issuer, + }, + } + + tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err := tokenClaims.SignedString(GetJWTSecret()) + return token, err +} diff --git a/pkg/setting/section.go b/pkg/setting/section.go index d3594cc..af4ea42 100644 --- a/pkg/setting/section.go +++ b/pkg/setting/section.go @@ -9,6 +9,12 @@ type ServerSettingS struct { WriteTimeout time.Duration } +type JWT struct { + Secret string + Issuer string + Expire time.Duration +} + type AppSettingS struct { DefaultPageSize int MaxPageSize int