feat: add logout

Co-authored-by: cxy004 <cxy004@qq.com>
Co-authored-by: wzt <w.zhongtao@qq.com>
This commit is contained in:
Paul Pan 2022-09-17 18:10:06 +08:00
parent f163768aae
commit ec660e706e
22 changed files with 209 additions and 79 deletions

View File

@ -18,6 +18,14 @@ func main() {
Name: "Paul",
Email: "i@0x7f.app",
},
{
Name: "cxy004",
Email: "cxy004@qq.com",
},
{
Name: "wzt",
Email: "w.zhongtao@qq.com",
},
},
Flags: []cli.Flag{
&cli.StringFlag{

3
go.mod
View File

@ -7,8 +7,8 @@ require (
github.com/gin-contrib/pprof v1.4.0
github.com/gin-contrib/zap v0.0.2
github.com/gin-gonic/gin v1.8.1
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.13.0
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.3
@ -29,6 +29,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect

9
go.sum
View File

@ -74,10 +74,13 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
@ -124,6 +127,8 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@ -299,6 +304,9 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
@ -733,6 +741,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -2,6 +2,7 @@ package user
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/service/user"
"github.com/gin-gonic/gin"
)
@ -20,8 +21,7 @@ type createRequest struct {
// @Param username formData string true "username"
// @Param nickname formData string true "nickname"
// @Param password formData string true "password"
// @Response 200 {object} e.Response "random string"
// @Security Authentication
// @Response 200 {object} e.Response "jwt token"
// @Router /v1/user/create [post]
func (h *handler) Create(c *gin.Context) {
req := new(createRequest)
@ -37,6 +37,21 @@ func (h *handler) Create(c *gin.Context) {
Password: req.Password,
}
id, err := h.userService.Create(createData)
e.Pong(c, err, id)
u, err := h.userService.Create(createData)
if err != e.Success {
e.Pong(c, err, nil)
return
}
version, err := h.userService.IncrVersion(u.ID)
if err != e.Success {
e.Pong(c, err, nil)
return
}
claim := &global.Claim{
UID: u.ID,
Version: version,
}
token, err := h.jwtService.SignClaim(claim)
e.Pong(c, err, token)
}

View File

@ -2,7 +2,6 @@ package user
import (
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/repo/model"
"github.com/WHUPRJ/woj-server/internal/service/user"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
@ -14,8 +13,6 @@ type Handler interface {
Create(c *gin.Context)
Login(c *gin.Context)
// List(c *gin.Context)
tokenNext(c *gin.Context, user *model.User)
}
type handler struct {
@ -32,6 +29,7 @@ func RouteRegister(g *global.Global, group *gin.RouterGroup) {
}
group.POST("/login", app.Login)
group.POST("/create", app.jwtService.Handler(), app.Create)
group.POST("/create", app.Create)
group.POST("/logout", app.jwtService.Handler(), app.Logout)
// group.GET("/", app.List)
}

View File

@ -2,6 +2,7 @@ package user
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/repo/model"
"github.com/gin-gonic/gin"
)
@ -18,7 +19,7 @@ type loginRequest struct {
// @Produce json
// @Param username formData string true "username"
// @Param password formData string true "password"
// @Response 200 {object} e.Response "random string"
// @Response 200 {object} e.Response "jwt token"
// @Router /v1/user/login [post]
func (h *handler) Login(c *gin.Context) {
req := new(loginRequest)
@ -40,5 +41,15 @@ func (h *handler) Login(c *gin.Context) {
}
// sign and return token
h.tokenNext(c, user)
version, err := h.userService.IncrVersion(user.ID)
if err != e.Success {
e.Pong(c, err, nil)
return
}
claim := &global.Claim{
UID: user.ID,
Version: version,
}
token, err := h.jwtService.SignClaim(claim)
e.Pong(c, err, token)
}

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/gin-gonic/gin"
)
// Logout
// @Summary logout
// @Description logout
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Response 200 {object} e.Response "nil"
// @Security Authentication
// @Router /v1/user/logout [post]
func (h *handler) Logout(c *gin.Context) {
claim, exist := c.Get("claim")
if !exist {
e.Pong(c, e.UserUnauthenticated, nil)
return
}
_, err := h.userService.IncrVersion(claim.(*global.Claim).UID)
e.Pong(c, err, nil)
}

View File

@ -1,18 +0,0 @@
package user
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/repo/model"
"github.com/gin-gonic/gin"
)
func (h *handler) tokenNext(c *gin.Context, user *model.User) {
claim := &global.Claim{
UID: user.ID,
UserName: user.UserName,
NickName: user.NickName,
}
token, err := h.jwtService.SignClaim(claim)
e.Pong(c, err, token)
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/repo/postgresql"
"github.com/WHUPRJ/woj-server/internal/repo/redis"
"github.com/WHUPRJ/woj-server/internal/router"
"github.com/WHUPRJ/woj-server/internal/service/jwt"
"go.uber.org/zap"
@ -17,9 +18,13 @@ import (
func Run(g *global.Global) error {
// Setup Database
g.Db = new(postgresql.PgRepo)
g.Db = new(postgresql.Repo)
g.Db.Setup(g)
// Setup Redis
g.Redis = new(redis.Repo)
g.Redis.Setup(g)
// Setup JWT
g.Jwt = jwt.NewJwtService(g)

View File

@ -20,6 +20,9 @@ const (
UserNotFound Err = 300
UserWrongPassword Err = 301
UserDuplicated Err = 302
UserUnauthenticated Err = 303
RedisError Err = 400
)
var msgText = map[Err]string{
@ -42,4 +45,7 @@ var msgText = map[Err]string{
UserNotFound: "User Not Found",
UserWrongPassword: "User Wrong Password",
UserDuplicated: "User Duplicated",
UserUnauthenticated: "User Unauthenticated",
RedisError: "Redis Error",
}

View File

@ -10,5 +10,6 @@ type Global struct {
Conf *Config
Stat *metrics.Metrics
Db Repo
Redis Repo
Jwt JwtService
}

View File

@ -8,16 +8,14 @@ import (
type Claim struct {
UID uint `json:"id"`
UserName string `json:"user_name"`
NickName string `json:"nick_name"`
Version int `json:"version"`
Version int64 `json:"version"`
jwt.RegisteredClaims
}
type JwtService interface {
ParseToken(tokenText string) (*Claim, e.Err)
SignClaim(claim *Claim) (string, e.Err)
// TODO: Validate(claim *Claim) bool
Validate(claim *Claim) bool
Handler() gin.HandlerFunc
}

View File

@ -1,12 +1,7 @@
package global
import (
"gorm.io/gorm"
)
type Repo interface {
Setup(*Global)
Get() *gorm.DB
Get() interface{}
Close() error
}

View File

@ -14,18 +14,18 @@ import (
"time"
)
var _ global.Repo = (*PgRepo)(nil)
var _ global.Repo = (*Repo)(nil)
type PgRepo struct {
type Repo struct {
db *gorm.DB
log *zap.Logger
}
func (r *PgRepo) Get() *gorm.DB {
func (r *Repo) Get() interface{} {
return r.db
}
func (r *PgRepo) Close() error {
func (r *Repo) Close() error {
db, err := r.db.DB()
if err != nil {
return err
@ -33,7 +33,7 @@ func (r *PgRepo) Close() error {
return db.Close()
}
func (r *PgRepo) Setup(g *global.Global) {
func (r *Repo) Setup(g *global.Global) {
r.log = g.Log
r.log.Info("Connecting to database...")
@ -78,14 +78,14 @@ func (r *PgRepo) Setup(g *global.Global) {
r.migrateDatabase()
}
func (r *PgRepo) migrateDatabase() {
func (r *Repo) migrateDatabase() {
r.log.Info("Auto Migrating database...")
_ = r.db.AutoMigrate(&model.User{})
}
// checkAlive deprecated
func (r *PgRepo) checkAlive(retry int) (*sql.DB, error) {
func (r *Repo) checkAlive(retry int) (*sql.DB, error) {
if retry <= 0 {
return nil, errors.New("all retries are used up. failed to connect to database")
}

View File

@ -0,0 +1,39 @@
package redis
import (
"context"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/go-redis/redis/v8"
"go.uber.org/zap"
)
var _ global.Repo = (*Repo)(nil)
type Repo struct {
client *redis.Client
log *zap.Logger
}
func (r *Repo) Setup(g *global.Global) {
r.log = g.Log
r.client = redis.NewClient(&redis.Options{
Addr: g.Conf.Redis.Address,
Password: g.Conf.Redis.Password,
DB: g.Conf.Redis.Db,
})
_, err := r.client.Ping(context.Background()).Result()
if err != nil {
r.log.Fatal("Redis ping failed", zap.Error(err))
return
}
}
func (r *Repo) Get() interface{} {
return r.client
}
func (r *Repo) Close() error {
return r.client.Close()
}

View File

@ -8,13 +8,14 @@ import (
func (s *service) Handler() gin.HandlerFunc {
return func(c *gin.Context) {
const tokenPrefix = "bearer "
tokenHeader := c.GetHeader("Authorization")
if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), "bearer ") {
if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), tokenPrefix) {
e.Pong(c, e.TokenEmpty, nil)
c.Abort()
return
}
token := tokenHeader[7:]
token := tokenHeader[len(tokenPrefix):]
claim, err := s.ParseToken(token)
if err != e.Success {
@ -23,11 +24,11 @@ func (s *service) Handler() gin.HandlerFunc {
return
}
// TODO: validate claim version
// if !s.Validate(claim) {
// e.Pong(c, e.TokenRevoked, nil)
// c.Abort()
// }
if !s.Validate(claim) {
e.Pong(c, e.TokenRevoked, nil)
c.Abort()
return
}
c.Set("claim", claim)
c.Next()

View File

@ -2,6 +2,7 @@ package jwt
import (
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/go-redis/redis/v8"
"go.uber.org/zap"
)
@ -9,6 +10,7 @@ var _ global.JwtService = (*service)(nil)
type service struct {
log *zap.Logger
redis *redis.Client
SigningKey []byte
ExpireHour int
}
@ -16,6 +18,7 @@ type service struct {
func NewJwtService(g *global.Global) global.JwtService {
return &service{
log: g.Log,
redis: g.Redis.Get().(*redis.Client),
SigningKey: []byte(g.Conf.WebServer.JwtSigningKey),
ExpireHour: g.Conf.WebServer.JwtExpireHour,
}

View File

@ -1,6 +1,7 @@
package jwt
import (
"context"
"fmt"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
@ -17,7 +18,7 @@ func (s *service) ParseToken(tokenText string) (*global.Claim, e.Err) {
token, err := jwt.ParseWithClaims(
tokenText,
&global.Claim{},
new(global.Claim),
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
@ -64,3 +65,12 @@ func (s *service) SignClaim(claim *global.Claim) (string, e.Err) {
}
return ss, e.Success
}
func (s *service) Validate(claim *global.Claim) bool {
curVersion, err := s.redis.Get(context.Background(), fmt.Sprintf("Version:%d", claim.UID)).Int64()
if err != nil {
s.log.Debug("redis.Get error", zap.Error(err))
return false
}
return curVersion == claim.Version
}

View File

@ -14,11 +14,11 @@ type CreateData struct {
Password string
}
func (s *service) Create(data *CreateData) (uint, e.Err) {
func (s *service) Create(data *CreateData) (*model.User, e.Err) {
hashed, err := bcrypt.GenerateFromPassword([]byte(data.Password), bcrypt.DefaultCost)
if err != nil {
s.log.Debug("bcrypt error", zap.Error(err), zap.String("password", data.Password))
return 0, e.InternalError
return nil, e.InternalError
}
user := &model.User{
@ -28,12 +28,12 @@ func (s *service) Create(data *CreateData) (uint, e.Err) {
IsEnabled: true,
}
if err := s.db.Get().Create(user).Error; err != nil {
if err := s.db.Create(user).Error; err != nil {
if strings.Contains(err.Error(), "duplicate key") {
return 0, e.UserDuplicated
return nil, e.UserDuplicated
}
s.log.Debug("create user error", zap.Error(err), zap.Any("data", data))
return 0, e.DatabaseError
return nil, e.DatabaseError
}
return user.ID, e.Success
return user, e.Success
}

View File

@ -11,7 +11,7 @@ import (
func (s *service) Login(data *model.User) (*model.User, e.Err) {
user := &model.User{UserName: data.UserName}
err := s.db.Get().Where(user).First(&user).Error
err := s.db.Where(user).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return user, e.UserNotFound
}

View File

@ -4,24 +4,29 @@ import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/repo/model"
"github.com/go-redis/redis/v8"
"go.uber.org/zap"
"gorm.io/gorm"
)
var _ Service = (*service)(nil)
type Service interface {
Create(data *CreateData) (uint, e.Err)
Create(data *CreateData) (*model.User, e.Err)
Login(data *model.User) (*model.User, e.Err)
IncrVersion(id uint) (int64, e.Err)
}
type service struct {
log *zap.Logger
db global.Repo
db *gorm.DB
redis *redis.Client
}
func NewUserService(g *global.Global) Service {
return &service{
log: g.Log,
db: g.Db,
db: g.Db.Get().(*gorm.DB),
redis: g.Redis.Get().(*redis.Client),
}
}

View File

@ -0,0 +1,17 @@
package user
import (
"context"
"fmt"
"github.com/WHUPRJ/woj-server/internal/e"
"go.uber.org/zap"
)
func (s *service) IncrVersion(id uint) (int64, e.Err) {
version, err := s.redis.Incr(context.Background(), fmt.Sprintf("Version:%d", id)).Result()
if err != nil {
s.log.Debug("redis.Incr error", zap.Error(err))
return -1, e.RedisError
}
return version, e.Success
}