feat: add JWT service
This commit is contained in:
parent
c6436125ef
commit
787fc39c29
@ -1,6 +1,8 @@
|
|||||||
WebServer:
|
WebServer:
|
||||||
Address: 0.0.0.0
|
Address: 0.0.0.0
|
||||||
Port: 8000
|
Port: 8000
|
||||||
|
JwtSigningKey: 'rq67SdQIRABhHq40'
|
||||||
|
JwtExpireHour: 12
|
||||||
|
|
||||||
Redis:
|
Redis:
|
||||||
Db: 0
|
Db: 0
|
||||||
|
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ require (
|
|||||||
github.com/gin-contrib/pprof v1.4.0
|
github.com/gin-contrib/pprof v1.4.0
|
||||||
github.com/gin-contrib/zap v0.0.2
|
github.com/gin-contrib/zap v0.0.2
|
||||||
github.com/gin-gonic/gin v1.8.1
|
github.com/gin-gonic/gin v1.8.1
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.13.0
|
github.com/prometheus/client_golang v1.13.0
|
||||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
|
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
|
||||||
|
2
go.sum
2
go.sum
@ -130,6 +130,8 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
|
|||||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
@ -3,17 +3,35 @@ package e
|
|||||||
const (
|
const (
|
||||||
Success Err = 0
|
Success Err = 0
|
||||||
Unknown Err = 1
|
Unknown Err = 1
|
||||||
|
|
||||||
InternalError Err = 100
|
InternalError Err = 100
|
||||||
InvalidParameter Err = 101
|
InvalidParameter Err = 101
|
||||||
NotFound Err = 102
|
NotFound Err = 102
|
||||||
DatabaseError Err = 103
|
DatabaseError Err = 103
|
||||||
|
|
||||||
|
TokenUnknown Err = 200
|
||||||
|
TokenEmpty Err = 201
|
||||||
|
TokenMalformed Err = 202
|
||||||
|
TokenTimeError Err = 203
|
||||||
|
TokenInvalid Err = 204
|
||||||
|
TokenSignError Err = 205
|
||||||
|
TokenRevoked Err = 206
|
||||||
)
|
)
|
||||||
|
|
||||||
var msgText = map[Err]string{
|
var msgText = map[Err]string{
|
||||||
Success: "Success",
|
Success: "Success",
|
||||||
Unknown: "Unknown error",
|
Unknown: "Unknown error",
|
||||||
|
|
||||||
InternalError: "Internal Error",
|
InternalError: "Internal Error",
|
||||||
InvalidParameter: "Invalid Parameter",
|
InvalidParameter: "Invalid Parameter",
|
||||||
NotFound: "Not Found",
|
NotFound: "Not Found",
|
||||||
DatabaseError: "Database Error",
|
DatabaseError: "Database Error",
|
||||||
|
|
||||||
|
TokenUnknown: "Unknown Error (Token)",
|
||||||
|
TokenEmpty: "Token Empty",
|
||||||
|
TokenMalformed: "Token Malformed",
|
||||||
|
TokenTimeError: "Token Time Error",
|
||||||
|
TokenInvalid: "Token Invalid",
|
||||||
|
TokenSignError: "Token Sign Error",
|
||||||
|
TokenRevoked: "Token Revoked",
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package global
|
|||||||
type ConfigWebServer struct {
|
type ConfigWebServer struct {
|
||||||
Address string `yaml:"Address"`
|
Address string `yaml:"Address"`
|
||||||
Port int `yaml:"Port"`
|
Port int `yaml:"Port"`
|
||||||
|
JwtSigningKey string `yaml:"JwtSigningKey"`
|
||||||
|
JwtExpireHour int `yaml:"JwtExpireHour"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigRedis struct {
|
type ConfigRedis struct {
|
||||||
|
35
internal/service/jwt/middleware.go
Normal file
35
internal/service/jwt/middleware.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) Handler() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
tokenHeader := c.GetHeader("Authorization")
|
||||||
|
if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), "bearer ") {
|
||||||
|
e.Pong(c, e.TokenEmpty, nil)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := tokenHeader[7:]
|
||||||
|
|
||||||
|
claim, err := s.ParseToken(token)
|
||||||
|
if err != e.Success {
|
||||||
|
e.Pong(c, err, nil)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: validate claim version
|
||||||
|
// if !s.Validate(claim) {
|
||||||
|
// e.Pong(c, e.TokenRevoked, nil)
|
||||||
|
// c.Abort()
|
||||||
|
// }
|
||||||
|
|
||||||
|
c.Set("claim", claim)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
11
internal/service/jwt/model.go
Normal file
11
internal/service/jwt/model.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import "github.com/golang-jwt/jwt/v4"
|
||||||
|
|
||||||
|
type Claim struct {
|
||||||
|
UID uint `json:"id"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
NickName string `json:"nick_name"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
32
internal/service/jwt/service.go
Normal file
32
internal/service/jwt/service.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Service = (*service)(nil)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
ParseToken(tokenText string) (*Claim, e.Err)
|
||||||
|
SignClaim(claim *Claim) (string, e.Err)
|
||||||
|
// TODO: Validate(claim *Claim) bool
|
||||||
|
|
||||||
|
Handler() gin.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
log *zap.Logger
|
||||||
|
SigningKey []byte
|
||||||
|
ExpireHour int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJwtService(g *global.Global) Service {
|
||||||
|
return &service{
|
||||||
|
log: g.Log,
|
||||||
|
SigningKey: []byte(g.Conf.WebServer.JwtSigningKey),
|
||||||
|
ExpireHour: g.Conf.WebServer.JwtExpireHour,
|
||||||
|
}
|
||||||
|
}
|
65
internal/service/jwt/token.go
Normal file
65
internal/service/jwt/token.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) ParseToken(tokenText string) (*Claim, e.Err) {
|
||||||
|
if tokenText == "" {
|
||||||
|
return nil, e.TokenEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(
|
||||||
|
tokenText,
|
||||||
|
&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"])
|
||||||
|
}
|
||||||
|
return s.SigningKey, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if ve, ok := err.(*jwt.ValidationError); ok {
|
||||||
|
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
|
||||||
|
return nil, e.TokenMalformed
|
||||||
|
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
|
||||||
|
// Token is either expired or not active yet
|
||||||
|
return nil, e.TokenTimeError
|
||||||
|
} else {
|
||||||
|
return nil, e.TokenInvalid
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
s.log.Warn("JWT Token Parse Error", zap.Error(err))
|
||||||
|
return nil, e.TokenUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.Valid {
|
||||||
|
c := token.Claims.(*Claim)
|
||||||
|
return c, e.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, e.TokenInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) SignClaim(claim *Claim) (string, e.Err) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
claim.IssuedAt = jwt.NewNumericDate(now)
|
||||||
|
claim.ExpiresAt = jwt.NewNumericDate(now.Add(time.Duration(s.ExpireHour) * time.Hour))
|
||||||
|
claim.ID = utils.RandomString(16)
|
||||||
|
// TODO: use per-user claim.Version to tracker invalidation
|
||||||
|
claim.NotBefore = jwt.NewNumericDate(time.Now())
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claim)
|
||||||
|
ss, err := token.SignedString(s.SigningKey)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("jwt.SignedString error", zap.Error(err))
|
||||||
|
return "", e.TokenSignError
|
||||||
|
}
|
||||||
|
return ss, e.Success
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user