上一篇已經知道如何透過 GraphQL對 Database 進行操作
接下來要在 Application 中加入身分認證的功能,使用的是 JWT(Json Web Token)
如果沒有接觸過 JWT,更多的細節請見關於 JWT 的參考資料
個人覺得這個教學其實蠻好的,雖然我 JWT 跟 Database 的部分都實作過了
但是很推薦給想做 backend 的新手參考
這個教學基本上跟實際運作的方式相差無幾,可以拿去應用在實戰上
開始實作前先執行 $ go get github.com/dgrijalva/jwt-go
取得 JWT package
完成後的資料夾如下
pkg/jwt/jwt.go
實作能夠產生與解析 JWT Token 的程式
package jwt
import (
"github.com/dgrijalva/jwt-go"
"log"
"time"
)
// secret key being used to sign tokens
var (
SecretKey = []byte("secret")
)
// GenerateToken generates a jwt token and assign a username to it's claims and return it
func GenerateToken(username string) (string, error) {
token := jwt.New(jwt.SigningMethodHS256)
/* Create a map to store our claims */
claims := token.Claims.(jwt.MapClaims)
/* Set token claims */
claims["username"] = username
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
tokenString, err := token.SignedString(SecretKey)
if err != nil {
log.Fatal("Error in Generating key")
return "", err
}
return tokenString, nil
}
// ParseToken parses a jwt token and returns the username in it's claims
func ParseToken(tokenStr string) (string, error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return SecretKey, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
username := claims["username"].(string)
return username, nil
} else {
return "", err
}
}
internal/users/users.go
實作註冊功能
package users
import (
"database/sql"
"github.com/glyphack/go-graphql-hackernews/internal/pkg/db/mysql"
"golang.org/x/crypto/bcrypt"
"log"
)
type User struct {
ID string `json:"id"`
Username string `json:"name"`
Password string `json:"password"`
}
func (user *User) Create() {
statement, err := database.Db.Prepare("INSERT INTO Users(Username,Password) VALUES(?,?)")
print(statement)
if err != nil {
log.Fatal(err)
}
hashedPassword, err := HashPassword(user.Password)
_, err = statement.Exec(user.Username, hashedPassword)
if err != nil {
log.Fatal(err)
}
}
//HashPassword hashes given password
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
//CheckPassword hash compares raw password with it's hashed values
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
internal/users/users.go
加入 Query 程式碼
//GetUserIdByUsername check if a user exists in database by given username
func GetUserIdByUsername(username string) (int, error) {
statement, err := database.Db.Prepare("select ID from Users WHERE Username = ?")
if err != nil {
log.Fatal(err)
}
row := statement.QueryRow(username)
var Id int
err = row.Scan(&Id)
if err != nil {
if err != sql.ErrNoRows {
log.Print(err)
}
return 0, err
}
return Id, nil
}
internal/auth/middleware.go
實作 HTTP server router middleware
只要使用到 API 都需要經過 middleware 認證
package auth
import (
"Go-GraphQL/internal/pkg/jwt"
"Go-GraphQL/internal/users"
"context"
"net/http"
"strconv"
)
var userCtxKey = &contextKey{"user"}
type contextKey struct {
name string
}
func Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("Authorization")
// Allow unauthenticated users in
if header == "" {
next.ServeHTTP(w, r)
return
}
//validate jwt token
tokenStr := header
username, err := jwt.ParseToken(tokenStr)
if err != nil {
http.Error(w, "Invalid token", http.StatusForbidden)
return
}
// create user and check if user exists in db
user := users.User{Username: username}
id, err := users.GetUserIdByUsername(username)
if err != nil {
next.ServeHTTP(w, r)
return
}
user.ID = strconv.Itoa(id)
// put it in context
ctx := context.WithValue(r.Context(), userCtxKey, &user)
// and call the next with our new context
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
// ForContext finds the user from the context. REQUIRES Middleware to have run.
func ForContext(ctx context.Context) *users.User {
raw, _ := ctx.Value(userCtxKey).(*users.User)
return raw
}
最後改寫 main.go
package main
import (
"Go-GraphQL/graph"
"Go-GraphQL/internal/auth"
mig "Go-GraphQL/internal/pkg/db/migrations/mysql"
"log"
"net/http"
"os"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi"
)
const defaultPort = "8080"
func main() {
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
router := chi.NewRouter()
router.Use(auth.Middleware())
mig.InitDB()
mig.Migrate()
server := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}}))
router.Handle("/", playground.Handler("GraphQL playground", "/query"))
router.Handle("/query", server)
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, router))
}