diff --git a/common/time/time.go b/common/time/time.go index b9f8680..1f373fc 100644 --- a/common/time/time.go +++ b/common/time/time.go @@ -12,8 +12,20 @@ const ( ) // MarshalJSON on Json Time format Time field with %Y-%m-%d %H:%M:%S -func (t Time) MarshalJSON() ([]byte, error) { +func (t *Time) MarshalJSON() ([]byte, error) { // 重写time转换成json之后的格式 - var tmp = fmt.Sprintf("\"%s\"", time.Time(t).Format(timeFormat)) + var tmp = fmt.Sprintf("\"%s\"", time.Time(*t).Format(timeFormat)) return []byte(tmp), nil } + +func (t *Time) UnmarshalJSON(data []byte) error { + // Ignore null, like in the main JSON package. + if string(data) == "null" { + return nil + } + // Fractional seconds are handled implicitly by Parse. + var err error + rawT, err := time.Parse(`"`+timeFormat+`"`, string(data)) + *t = Time(rawT) + return err +} diff --git a/controller/auth/auth.go b/controller/auth/auth.go index e3d4e4b..e46bf68 100644 --- a/controller/auth/auth.go +++ b/controller/auth/auth.go @@ -31,7 +31,7 @@ func NewController(app *fiber.App) *Controller { // @Accept json // @Produce json // @Param vo body dto.Login true "用户登录" -// @Success 200 {object} response.Response{data=string} +// @Success 200 {object} response.Response{data=vo.Login} // @Failure default {object} errorx.CodeErrorResponse // @Router /auth/login [post] func (c *Controller) Login() { @@ -42,11 +42,39 @@ func (c *Controller) Login() { return ctx.JSON(err) } - token, err := auth.Services.Login(login) + result, err := auth.Services.Login(login) if err = errorx.ParseError(err); err != nil { return ctx.JSON(err) } - return ctx.JSON(response.NewResponse(token)) + return ctx.JSON(response.NewResponse(result)) + }) +} + +// RefreshToken 刷新令牌 +// +// @Summary 刷新令牌 +// @Description 刷新令牌 +// @Tags Auth +// @Accept json +// @Produce json +// @Param vo body dto.RefreshToken true "刷新令牌" +// @Success 200 {object} response.Response{data=vo.Login} +// @Failure default {object} errorx.CodeErrorResponse +// @Router /auth/refreshToken [post] +func (c *Controller) RefreshToken() { + c.Router.Post("refreshToken", func(ctx *fiber.Ctx) error { + refresh := &dto.RefreshToken{} + err := ctx.BodyParser(refresh) + if err = errorx.ParseError(err); err != nil { + return ctx.JSON(err) + } + + result, err := auth.Services.RefreshToken(refresh.RefreshToken) + if err = errorx.ParseError(err); err != nil { + return ctx.JSON(err) + } + + return ctx.JSON(response.NewResponse(result)) }) } diff --git a/docs/docs.go b/docs/docs.go index a44ded0..8725eee 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -60,7 +60,59 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "type": "string" + "$ref": "#/definitions/vo.Login" + } + } + } + ] + } + }, + "default": { + "description": "", + "schema": { + "$ref": "#/definitions/errorx.CodeErrorResponse" + } + } + } + } + }, + "/auth/refreshToken": { + "post": { + "description": "刷新令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "刷新令牌", + "parameters": [ + { + "description": "刷新令牌", + "name": "vo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RefreshToken" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.Login" } } } @@ -642,6 +694,15 @@ const docTemplate = `{ } } }, + "dto.RefreshToken": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string", + "example": "0123456789ABCDEFG" + } + } + }, "errorx.CodeErrorResponse": { "type": "object", "properties": { @@ -742,6 +803,21 @@ const docTemplate = `{ "example": "OK" } } + }, + "vo.Login": { + "type": "object", + "properties": { + "refreshToken": { + "description": "refreshToken 刷新令牌", + "type": "string", + "example": "0123456789ABCDEFG" + }, + "token": { + "description": "token 用户令牌", + "type": "string", + "example": "0123456789ABCDEFG" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index e200d70..b129fa2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -52,7 +52,59 @@ "type": "object", "properties": { "data": { - "type": "string" + "$ref": "#/definitions/vo.Login" + } + } + } + ] + } + }, + "default": { + "description": "", + "schema": { + "$ref": "#/definitions/errorx.CodeErrorResponse" + } + } + } + } + }, + "/auth/refreshToken": { + "post": { + "description": "刷新令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "刷新令牌", + "parameters": [ + { + "description": "刷新令牌", + "name": "vo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RefreshToken" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.Login" } } } @@ -634,6 +686,15 @@ } } }, + "dto.RefreshToken": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string", + "example": "0123456789ABCDEFG" + } + } + }, "errorx.CodeErrorResponse": { "type": "object", "properties": { @@ -734,6 +795,21 @@ "example": "OK" } } + }, + "vo.Login": { + "type": "object", + "properties": { + "refreshToken": { + "description": "refreshToken 刷新令牌", + "type": "string", + "example": "0123456789ABCDEFG" + }, + "token": { + "description": "token 用户令牌", + "type": "string", + "example": "0123456789ABCDEFG" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f5d40b3..3704a50 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -9,6 +9,12 @@ definitions: example: "12341234" type: string type: object + dto.RefreshToken: + properties: + refreshToken: + example: 0123456789ABCDEFG + type: string + type: object errorx.CodeErrorResponse: properties: code: @@ -78,6 +84,17 @@ definitions: example: OK type: string type: object + vo.Login: + properties: + refreshToken: + description: refreshToken 刷新令牌 + example: 0123456789ABCDEFG + type: string + token: + description: token 用户令牌 + example: 0123456789ABCDEFG + type: string + type: object info: contact: email: 919411476@qq.com @@ -112,7 +129,7 @@ paths: - $ref: '#/definitions/response.Response' - properties: data: - type: string + $ref: '#/definitions/vo.Login' type: object default: description: "" @@ -121,6 +138,37 @@ paths: summary: 用户登录 tags: - Auth + /auth/refreshToken: + post: + consumes: + - application/json + description: 刷新令牌 + parameters: + - description: 刷新令牌 + in: body + name: vo + required: true + schema: + $ref: '#/definitions/dto.RefreshToken' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/vo.Login' + type: object + default: + description: "" + schema: + $ref: '#/definitions/errorx.CodeErrorResponse' + summary: 刷新令牌 + tags: + - Auth /casbin/getUserRoles: get: consumes: diff --git a/model/dto/auth.go b/model/dto/auth.go index e2cc43e..0db4191 100644 --- a/model/dto/auth.go +++ b/model/dto/auth.go @@ -1,6 +1,15 @@ package dto +// Login +// @Param account body string true "用户账号(account)" +// @Param password body string true "用户密码" type Login struct { Account string `json:"account" example:"root"` Password string `json:"password" example:"12341234"` } + +// RefreshToken +// @Param refreshToken body string true "刷新令牌" +type RefreshToken struct { + RefreshToken string `json:"refreshToken" example:"0123456789ABCDEFG"` +} diff --git a/model/vo/auth.go b/model/vo/auth.go new file mode 100644 index 0000000..6995bc3 --- /dev/null +++ b/model/vo/auth.go @@ -0,0 +1,8 @@ +package vo + +type Login struct { + // token 用户令牌 + Token string `json:"token" example:"0123456789ABCDEFG"` + // refreshToken 刷新令牌 + RefreshToken string `json:"refreshToken" example:"0123456789ABCDEFG"` +} diff --git a/services/auth/auth.go b/services/auth/auth.go index bdd7e22..e8ce024 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -1,15 +1,26 @@ package auth import ( + "context" "errors" - "gofiber.study.skcks.cn/common/errorx" + "github.com/goccy/go-json" + "gofiber.study.skcks.cn/common/logger" + "gofiber.study.skcks.cn/common/utils" "gofiber.study.skcks.cn/global" "gofiber.study.skcks.cn/model/dto" "gofiber.study.skcks.cn/model/generic/models" + "gofiber.study.skcks.cn/model/vo" + "time" ) var ( - Failed = errors.New("账号或密码错误") + Failed = errors.New("账号或密码错误") + InvalidRefreshToken = errors.New("刷新令牌已失效") +) + +const ( + Separator = ":" + RefreshTokenPrefix = "RefreshToken" + Separator ) type Service struct { @@ -21,7 +32,19 @@ func InitService() { Services = &Service{} } -func (s *Service) Login(login *dto.Login) (token string, err error) { +func (s *Service) generateAndSaveRefreshToken(user *models.User) (refreshToken string, err error) { + refreshToken, err = global.GetNanoId() + if err != nil { + return + } + + expire := time.Duration(global.Config.Jwt.Expire*2) * time.Second + ctx := context.Background() + global.Redis.Set(ctx, RefreshTokenPrefix+refreshToken, utils.Json(user), expire) + return +} + +func (s *Service) Login(login *dto.Login) (result *vo.Login, err error) { user := &models.User{Account: login.Account, Password: login.Password} exist, err := global.DataSources.Get(user) if err != nil { @@ -29,14 +52,61 @@ func (s *Service) Login(login *dto.Login) (token string, err error) { } if !exist { - return token, Failed + return nil, Failed } - token, err = global.GetToken(global.UserClaims{ + token, err := global.GetToken(global.UserClaims{ Id: user.Id, Account: user.Account, }) - err = errorx.ParseError(err) - return + if err != nil { + return + } + + refreshToken, err := s.generateAndSaveRefreshToken(user) + + return &vo.Login{ + Token: token, + RefreshToken: refreshToken, + }, err +} + +func (s *Service) RefreshToken(refreshToken string) (result *vo.Login, err error) { + ctx := context.Background() + data, err := global.Redis.Get(ctx, RefreshTokenPrefix+refreshToken).Result() + if err != nil { + return nil, InvalidRefreshToken + } + + global.Redis.Del(ctx, RefreshTokenPrefix+refreshToken) + + cache := &models.User{} + err = json.Unmarshal([]byte(data), cache) + if err != nil { + return nil, InvalidRefreshToken + } + + user := &models.User{Id: cache.Id, Account: cache.Account} + exist, err := global.DataSources.Get(user) + if !exist { + logger.Log.Infof("未能从 %s 找到用户信息", RefreshTokenPrefix+refreshToken) + return nil, InvalidRefreshToken + } + + token, err := global.GetToken(global.UserClaims{ + Id: user.Id, + Account: user.Account, + }) + + if err != nil { + return + } + + refreshToken, err = s.generateAndSaveRefreshToken(user) + + return &vo.Login{ + Token: token, + RefreshToken: refreshToken, + }, err }