Roślina abo rośne, albo umiera. Podobnie jest z wiedzą ludzi.
Nieznany
Świat aplikacji internetowych ewoluował w kierunku rozproszonej architektury komunikującej się między modułami za pomocą interfejsów, np REST (Representational State Transfer). Podejście to umożliwia efektywną komunikację między klientem a serwerem. 
Ten artykuł zawiera „szczegółowy” przewodnik dotyczący implementacji prostego serwera HTTP w języku Go obsługującego protokół REST i umożliwiającego autoryzację użytkownika za pomocą tokenów JWT. 
Bogata biblioteka standardowa Go, oraz liczne biblioteki dostępne w publicznych repozytoriach, sprawiają, że tworzenie wspomnianej implementacji serwera w Go jest niesamowicie łatwe i intuicyjne. Moje dotychczasowe doświadczenia podpowiadają mi, że była to najprzyjemniejsza implementacja tego typu prototypu z jaką miałem do czynienia. 
Czego się nauczysz? W trakcie tego poradnika dowiesz się:
REST,HTTP,HTTP w formacie RESTJWTCałość projektu podzieliłem na kilka plików, by logicznie podzielić implementację. Struktura projektu wygląda następująco:
├── go.mod 
├── go.sum 
├── main.go 
├── README.md 
└── service 
   └── v1 
       └── rest 
           ├── handlers.go 
           ├── server.go 
           ├── tokens.go 
           └── types.go
Jeśli nie wiesz jak zainicjować projekt podpowiadam
go mod init  prototype
Wynikiem powyżej komendy jest inicjalizacja projektu i utworzenie plików go.mod i go.sum.
Zaczynając od rzeczy najbardziej podstawowych, plik types.go zawierający stałe, zmienne, i typy wykorzystywane w projekcie:
# FILE ./service/v1/rest/types.go
package v1rest
const Version string = "v1.0.0"
var SECRET = []byte("--== HideMeFromPryingEyes ==--")
var userMap = map[string]string{
    "admin": "haslo123",
}
type User struct {
    Username string `json:"username"`
    Password string `json:"password"`
}
type VersionMsg struct {
    Version string `json:version`
}
Jak widzisz w prototypie znajduje się wiele rzeczy, na widok których krwawią oczy. Z całą pewnością wartość przypisana do zmiennej SECRET nie powinna się tu znaleźć, podobnie zresztą jak userMap zawierająca mapę z użytkownikami i hasłami. Produkcyjna implementacja powinna wykorzystywać do powyższych odpowiednio zmienne środowiskowe i bazę danych. Ponadto nigdy nie przechowuj haseł użytkownika w niezaszyfrowanej formie! 
Plik handlers.go zawiera obsługę żądań o jakie użytkownik będzie mógł wysyłać do naszego prototypowego serwera.
# FILE ./service/v1/rest/handlers.go
package v1rest
import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)
func loginHandler(writer http.ResponseWriter, request *http.Request) {
    switch request.Method {
    case "POST":
        var user User
        err := json.NewDecoder(request.Body).Decode(&user)
        if err != nil {
            fmt.Fprintf(writer, "Invalid or corrupted request!")
            return
        }
        if userMap[user.Username] == "" || userMap[user.Username] != user.Password {
            fmt.Fprintf(writer, "Not enough mana!")
            return
        }
        token, err := createJWT(user.Username)
        if err != nil {
            fmt.Fprintf(writer, "%s", err)
        }
        fmt.Fprintf(writer, token)
        return
    default:
        fmt.Fprintf(writer, "%s is not implemented.", request.Method)
        return
    }
}
func versionHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    err := validateJWT(w, r)
    if err != nil {
        w.WriteHeader()
        resp := make(map[string]string)
        resp["message"] = fmt.Sprint(err)
        jsonResp, err := json.Marshal(resp)
        if err != nil {
            log.Fatalf("Error happened in JSON marshal. Err: %s", err)
        }
        w.Write(jsonResp)
        return
    }
    w.WriteHeader(http.StatusOK)
    data := VersionMsg{Version: Version}
    jsonData, err := json.Marshal(data)
    if err != nil {
        fmt.Printf("Marshaling data failed: %s", err.Error())
    }
    w.Write(jsonData)
}
Nasz serwer posiada obsługę dwóch żądań: logowania (loginHandler) i sprawdzenia wersji serwera (versionHandler). Przed wykonaniem zapytania o numer wersji serwera będziemy musieli się uwierzytelnić metodąloginHandler. Po sprawdzeniu, czy nasza nazwa użytkownika i hasło znajdują się w mapie userMap zwróci nam token, niezbędny do obsługi innych żądań, oraz kod odpowiedzi http.StatusOK. W przypadku niezgodności nazwy pary nazwy użytkownika i hasła zwrócony zostanie błąd http.StatusUnauthorized, a zamiast token otrzymamy opis błędu.
Implementacja odpowiedzialna za tworzenie i weryfikację tokenów znajduje się w pliku tokens.go
# FILE ./service/v1/rest/tokens.go
package v1rest
import (
    "errors"
    "fmt"
    "log"
    "net/http"
    "time"
    "github.com/golang-jwt/jwt/v5"
)
func createJWT(username string) (string, error) {
    token := jwt.New(jwt.SigningMethodHS512)
    claims := token.Claims.(jwt.MapClaims)
    claims["exp"] = time.Now().Add(1 * time.Minute).Unix()
    claims["authorized"] = true
    claims["user"] = username
    tokenStr, err := token.SignedString(SECRET)
    if err != nil {
        fmt.Errorf("Something went wrong during token creation process: %s", err.Error())
        return "", err
    }
    return tokenStr, nil
}
func validateJWT(w http.ResponseWriter, r *http.Request) (err error) {
    if r.Header["Token"] == nil {
        log.Println("Token is not present in HEADER.")
        return errors.New("Token is not present in HEADER.")
    }
    keyFunc := func(token *jwt.Token) (interface{}, error) {
        /* Receive the parsed token.
         * Return the cryptographic key for verifying the signature.
         */
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            log.Println("There was an error in parsing")
            return nil, fmt.Errorf("There was an error in parsing")
        }
        return SECRET, nil
    }
    token, err := jwt.Parse(r.Header["Token"][0], keyFunc)
    if token == nil || err != nil {
        log.Println("There was an error during token parsing.")
        return errors.New("There was an error during token parsing.")
    }
    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        fmt.Fprintf(w, "There was an error during claims parsing.")
        return errors.New("There was an error during claims parsing.")
    }
    return nil
}
Metoda createJWT tworzy nam token zgodny ze standardem RFC7519, którego ważność upływa po 1 minucie. Krótki czas ważności tokena może prowadzić do częstego generowania nowych tokenów wprowadzając dodatkowe obciążenie systemu. W praktyce, czas ważności jest zazwyczaj dłuższy, ale zależy to od konkretnych wymagań projektu. Dodatkowo pamiętaj, że klucz używany do podpisywania JWT, w naszym przypadku SECRET, powinien być przechowywany w sposób bezpieczny. 
Metoda validateJWT sprawdza, czy nagłówek żądanie zawiera w sobie token, a następnie weryfikuje poprawność tokena, oraz czy nie upłynęła jego ważność. 
Przedostatnim krokiem jest połączenie wszystkich elementów w jedną całość w pliku server.go:
# FILE ./service/v1/rest/server.go
package v1rest
import (
    "errors"
    "fmt"
    "net/http"
)
type HTTPRestServer struct {
    server *http.Server
}
func (srv *HTTPRestServer) Configure(host, port string) {
    /* Zasadniczo serwer HTTP `mux` zawiera mapę uchwytów przypisaną do ścieżek.
     */
    mux := http.NewServeMux()
    mux.HandleFunc("/api/v1/version", versionHandler)
    mux.HandleFunc("/api/v1/login", loginHandler)
    srv.server = &http.Server{
        Addr:    host + ":" + port,
        Handler: mux,
    }
}
func (srv *HTTPRestServer) Start() {
    /* Starts HTTPRestServer as a goroutine. */
    go func() {
        err := srv.server.ListenAndServe()
        if errors.Is(err, http.ErrServerClosed) {
            fmt.Printf("HTTP REST Server is closed.\n")
        } else if err != nil {
            fmt.Printf("HTTP REST Server error while listening, %s\n", err)
        }
    }()
}
func (srv *HTTPRestServer) Stop() error {
    return srv.server.Shutdown(nil)
}
Tworzymy w nim nowy typ HTTPRestServer reprezentującą nasz serwer. Posiada on 3 metody
- Start() służącą do uruchomienia serwera jako osobna goroutyna
- Stop() do ‘eleganckiego’ zatrzymania działającego serwera
- Configure(host, port string) która tworzy nowego obsługującego mux, dodaje mu uchwyty przypisane do ścieżek wywołań, oraz tworzy instancję http.Server z zadanym HOST i PORT i spina z nią naszego obsługującego mux. Ilekroć nasz serwer otrzyma żądanie opisane ścieżką /api/v1/version lub /api/v1/login odpowiedni uchwyt zostanie uruchomiony do obsługi żądania.
Ostatnim krokiem jest stworzenie pliku main.go by użyć nasz prototyp w praktyce:
FILE ./main.go
package main
import (
    v1rest "prototype/service/v1/rest"
    "log"
    "os"
    "os/signal"
    "sync"
    "syscall"
)
const (
    HOST string = "127.0.0.1"
    PORT string = "3500"
)
func main() {
    var wg sync.WaitGroup
    restServer := v1rest.HTTPRestServer{}
    restServer.Configure(HOST, PORT)
    restServer.Start()
    // We want a server to gracefully shutdown after receiving
    // a SIGTERM, or a SIGINT (Ctrl+C) signal.
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
    wg.Add(1)
    go func() {
        defer wg.Done()
        sig := <-sigs
        log.Printf("Received %s signal, terminating.\n", sig)
        err := restServer.Stop()
        if err != nil {
            log.Printf("Error while stopping HTTP Rest Server: %s", err)
        }
    }()
    wg.Wait() // The program will wait here until Ctrl+C is pressed.
}
W pliku main.go tworzymy instancję HTTPRestServer i przypisujemy ją do zmiennej restServer, konfigurujemy ją i uruchamiamy. Reszta pliku to obsługa sygnałów przerwania, byśmy mieli możliwość zatrzymania serwera po naciśnięciu kombinacji klawiszy Ctrl+C. 
Czas uruchomić nasz program. Wszystkie komendy poniżej wykonuję w systemie Linux. Jeśli używasz innego systemu operacyjnego musisz sobie jakoś poradzić. W terminalu wydajemy polecenie 
go run .
po czym w osobnym terminalu możemy spróbować komunikacji z naszym serwerem:
TOKEN=$(curl --header "Content-Type: application/json" --request POST --data '{"username":"admin","password":"haslo123"}' http://localhost:3500/api/v1/login)
W wyniku żądania powinniśmy otrzymać token, który zostanie zapisany do zmiennej środowiskowej TOKEN. Możemy spróbować otrzymać wersję serwera 
curl -v -H 'Accept: application/json' -H "Token: ${TOKEN}" http://localhost:3500/api/v1/version ```
W wyniku wydania powyższej komendy powinniśmy otrzymać coś w podobnego do:
*   Trying 127.0.0.1:3500... 
* Connected to localhost (127.0.0.1) port 3500 (#0) 
> GET /api/v1/version HTTP/1.1 
> Host: localhost:3500 
> User-Agent: curl/7.88.1 
> Accept: application/json 
> Token: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2OTk2Mjc5MTgsInVzZXIiOiJzZWJhc3RpYW4ifQ.Hqnd82-Gb52CRTN2UQJBD4Qb3DY2L18kjIpSPADu3nnY4HV6h6UkXB2exhU85YH5IiVJcvoMb7MhaXxDo_vWHg 
>  
< HTTP/1.1 200 OK 
< Content-Type: application/json 
< Date: Fri, 10 Nov 2023 14:51:01 GMT 
< Content-Length: 20 
<  
* Connection #0 to host localhost left intact 
{"Version":"v1.0.0"}
Widzimy w odpowiedzi serwera jej status HTTP/1.1 200 OK, oraz wersję serwera {"Version":"v1.0.0"}. Wszystko działa. Po upływie minuty to samo żądanie powinno zamiast wersji serwera zwrócić nam:
*   Trying 127.0.0.1:3500... 
* Connected to localhost (127.0.0.1) port 3500 (#0) 
> GET /api/v1/version HTTP/1.1 
> Host: localhost:3500 
> User-Agent: curl/7.88.1 
> Accept: application/json 
> Token: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2OTk2MjgxODAsInVzZXIiOiJzZWJhc3RpYW4ifQ.2DKWwAEYlbtOa93exY0B76i9yqY5XQft9ouoylHF2zwiS2wc5IP8ckIvlxBa41_yfqTONo4BUDBIpro-uulTDg 
>  
< HTTP/1.1 401 Unauthorized 
< Content-Type: application/json 
< Date: Fri, 10 Nov 2023 14:56:21 GMT 
< Content-Length: 54 
<  
* Connection #0 to host localhost left intact 
{"message":"There was an error during token parsing."}
Mamy to. Nasz serwer działa i poprawnie obsługuje żądania. Jeśli chodzi o ten poradnik to tu chciałem zakończyć. Wiesz już jak stworzyć prosty serwer HTTP z obsługą JWT i REST w języku Go. Jeśli chcesz bawić się dalej to podsuwam Ci kilka pomysłów na dalszy rozwój projektu. W dalszych krokach rozwoju prototypu warto zająć się:
TLS,HOST i PORT poza kod,