Skip to content

Commit

Permalink
feat: implement for jwt token auth
Browse files Browse the repository at this point in the history
  • Loading branch information
kelein committed Nov 27, 2023
1 parent 94ef20b commit e631311
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The way to go with a cookbook 🍳
- [ ] [geek-design-patterns](https://time.geekbang.org/column/article/165114)
- [ ] [golang101](https://gfw.go101.org/)
- [ ] [master-to-go](https://github.com/aceld/golang)
- [ ] [The complete gRPC course](https://dev.to/techschoolguru/the-complete-grpc-course-protobuf-go-java-2af6)

## References

Expand Down
16 changes: 15 additions & 1 deletion devto-grpc/cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"flag"
"fmt"
"log"
Expand All @@ -20,7 +21,10 @@ var imgdir = flag.String("imgdir", "./../tests/imgs", "the image store dir")
func main() {
flag.Parse()

server := grpc.NewServer()
server := grpc.NewServer(
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
)
laptopStore := store.NewMemoryLaptopStore()
imageStore := store.NewDiskImageStore(*imgdir)
rateStore := store.NewMemoryRateStore()
Expand All @@ -37,3 +41,13 @@ func main() {
log.Fatalf("start server failed: %v", err)
}
}

func unaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
log.Printf("=> unary interceptor: %v", info.FullMethod)
return handler(ctx, req)
}

func streamInterceptor(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
log.Printf("=> stream interceptor: %v", info.FullMethod)
return handler(server, stream)
}
42 changes: 42 additions & 0 deletions devto-grpc/model/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package model

import (
"fmt"

"golang.org/x/crypto/bcrypt"
)

// User represents request auth info
type User struct {
Username string
Password string
Role string
}

// NewUser creates a new User instance with username and password
func NewUser(username, password, role string) (*User, error) {
cryptPass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("failed to generate crypto password: %v", err)
}
return &User{
Role: role,
Username: username,
Password: string(cryptPass),
}, nil
}

// Authed check whether the password is correct
func (u *User) Authed(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}

// Clone returns a clone of this user
func (u *User) Clone() *User {
return &User{
Username: u.Username,
Password: u.Password,
Role: u.Role,
}
}
43 changes: 43 additions & 0 deletions devto-grpc/pkg/jwtool/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package jwtool

import (
"time"

"github.com/dgrijalva/jwt-go"

"cookbook/devto-grpc/model"
)

// Manager for JWT authentication
type Manager struct {
secretKey string
expired time.Duration
}

// NewManager creates a new JWT Manager
func NewManager(secretKey string, expired time.Duration) *Manager {
return &Manager{
secretKey: secretKey,
expired: expired,
}
}

// Generate create a user token
func (m *Manager) Generate(user *model.User) (string, error) {
claims := UserClaims{
Username: user.Username,
Role: user.Role,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(m.expired).Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(m.secretKey))
}

// UserClaims stands for user claim
type UserClaims struct {
Username string `json:"username"`
Role string `json:"role"`
jwt.StandardClaims
}
5 changes: 5 additions & 0 deletions devto-grpc/service/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,8 @@ func NewLaptop() *repo.Laptop {
UpdateAt: timestamppb.Now(),
}
}

// RandomLaptopScore returns a random score
func RandomLaptopScore() float64 {
return float64(randInt(1, 10))
}
3 changes: 2 additions & 1 deletion devto-grpc/service/laptop_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ func startTestLaptopServer(t *testing.T, laptopStore store.LaptopStore, imgStore
t.Helper()

server := grpc.NewServer()
svc := NewLaptopServer(laptopStore, imgStore)
rateStore := store.NewMemoryRateStore()
svc := NewLaptopServer(laptopStore, imgStore, rateStore)
repo.RegisterLaptopServiceServer(server, svc)

lis, err := net.Listen("tcp", ":0")
Expand Down
55 changes: 55 additions & 0 deletions devto-grpc/service/laptop_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,62 @@ func (server *LaptopServer) UploadImage(stream repo.LaptopService_UploadImageSer
// RateLaptop rate laptop score via streaming
func (server *LaptopServer) RateLaptop(stream repo.LaptopService_RateLaptopServer) error {
for {
err := ctxErr(stream.Context())
if err != nil {
return err
}

req, err := stream.Recv()
if err == io.EOF {
log.Print("no more data")
break
}
if err != nil {
return logErr(status.Errorf(codes.Unknown, "receive stream error: %v", err))
}

score := req.GetScore()
laptopID := req.GetLaptopId()
log.Printf("received request id=%v, score=%v", laptopID, score)

got, err := server.laptopStore.Find(laptopID)
if err != nil {
return logErr(status.Errorf(codes.Internal, "find laptop error: %v", err))
}
if got == nil {
return logErr(status.Errorf(codes.NotFound, "laptop not found: %v", err))
}

rate, err := server.rateStore.Add(laptopID, score)
if err != nil {
return logErr(status.Errorf(codes.Internal, "rating laptop error: %v", err))
}
res := &repo.RateLaptopResponse{
LaptopId: laptopID,
RatedCount: rate.Count,
ScoreAverage: rate.Sum / float64(rate.Count),
}
if err := stream.Send(res); err != nil {
return logErr(status.Errorf(codes.Unknown, "send stream error: %v", err))
}
}
return nil
}

func ctxErr(ctx context.Context) error {
switch ctx.Err() {
case context.Canceled:
return logErr(status.Error(codes.Canceled, "request canceld"))
case context.DeadlineExceeded:
return logErr(status.Error(codes.DeadlineExceeded, "deadlin exceeded"))
default:
return nil
}
}

func logErr(err error) error {
if err != nil {
log.Print(err)
}
return err
}
3 changes: 2 additions & 1 deletion devto-grpc/service/laptop_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ func TestLaptopServer_CreateLaptop(t *testing.T) {

req := &repo.CreateLaptopRequest{Laptop: tt.laptop}
imgStore := store.NewDiskImageStore("../tests/imgs")
server := NewLaptopServer(tt.store, imgStore)
rateStore := store.NewMemoryRateStore()
server := NewLaptopServer(tt.store, imgStore, rateStore)
res, err := server.CreateLaptop(context.Background(), req)
t.Logf("CreateLaptop() code=[%v], res=[%v], err=[%v]", tt.code, res, err)
if tt.code == codes.OK {
Expand Down
49 changes: 49 additions & 0 deletions devto-grpc/store/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package store

import (
"errors"
"sync"

"cookbook/devto-grpc/model"
)

// UserStore Errors
var (
ErrUserAlreadyExists = errors.New("user already exists")
)

// UserStore store user information
type UserStore interface {
Save(user *model.User) error
Find(username string) (*model.User, error)
}

// MemoryUserStore store user in memory
type MemoryUserStore struct {
mutex sync.RWMutex
users map[string]*model.User
}

// Save stores user in memory
func (s *MemoryUserStore) Save(user *model.User) error {
s.mutex.Lock()
defer s.mutex.Unlock()

if s.users[user.Username] == nil {
return ErrAlreadyExists
}
s.users[user.Username] = user.Clone()
return nil
}

// Find query user by username
func (s *MemoryUserStore) Find(username string) (*model.User, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()

user := s.users[username]
if user == nil {
return nil, nil
}
return user.Clone(), nil
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ module cookbook
go 1.19

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/golang/protobuf v1.5.3
github.com/google/uuid v1.4.0
github.com/jedib0t/go-pretty/v6 v6.4.9
github.com/jinzhu/copier v0.4.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.12.0
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.31.0
)
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
Expand Down Expand Up @@ -30,6 +32,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down

0 comments on commit e631311

Please sign in to comment.