Щоб щось зрозуміти іноді легше писати ніж читати (В мене була потреба зрозуміти як працює middleware jwt авторизації, і я цей код читати не міг. Довелось для розминки написати аналогічне, трохи помогло).
Якщо вам цікаво що таке JWT і для чого, то в двох словах – це можливість видати комусь право доступу до чогось без бази даних де б писало що ми йому таке право давали. Тобто сервер на якому користувач авторизується, і сервер до якого він отримує доступ – це можуть бути два абсолютно окремі сервери, які не те що не мають спільної бази даних, вони навіть не спілкуються мережею. Головне – правильні ключі.
Напишемо наступне супер просте API:
POST /login {user: “”, password: “”} – віддає нам JWT токен для дозволу запису
GET / – віддає нам список записів
POST / – з заголовком “Authorization: Bearer ” дозволяє додати новий запис до списку, якщо ми авторизовані.
Для початку зробимо все без авторизації:
package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" ) func main() { initDB() http.HandleFunc("/", handler) // За головну сторінку відповідатиме функція handler fmt.Println("Listening at 8080") http.ListenAndServe(":8080", nil) // Запускаємо сервер на якомусь порті } // "база даних" var log []string // Заповнюємо "базу" якимись даними func initDB() { log = make([]string, 0) log = append(log, "Hello") log = append(log, "World") } func handler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { data, err := json.Marshal(log) // серіалізуємо базу в JSON if errorHandler(w, err, http.StatusInternalServerError) { // якщо була помилка return } w.Write(data) } else if r.Method == "POST" { bodybytes, err := ioutil.ReadAll(r.Body) // Читаємо все що передано if errorHandler(w, err, http.StatusInternalServerError) { return } log = append(log, string(bodybytes)) // і додаємо як текст в базу даних w.Write(bodybytes) // і повертаємо що додали } else { // невідомий метод errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed) } } // Функція яка якщо отримує помилку пише у відповідь текст помилки з кодом, і повертає true, // а якщо не було - просто повертає false. func errorHandler(w http.ResponseWriter, err error, code int) bool { if err == nil { return false } fmt.Println(err) msg, _ := json.Marshal(map[string]string{ "error": err.Error(), }) w.Write(msg) return true }
Запишемо це в файл наприклад main.go
і запустимо go run main.go
. Тепер можна перевірити все командами:
curl http://localhost:8080/ # Дає ["Hello","World"] curl -X POST http://localhost:8080/ -d "it works!" # Дає it works! curl http://localhost:8080/ # Дає ["Hello","World","it works!"]
Якщо працює – пора генерувати ключі. Бо ми ж не хочемо щоб будь-хто міг писати в нашу “базу даних”. Ключі в асиметричній криптографії завжди генеруються парами, один публічний, один приватний.
openssl genrsa -out key.rsa 1024 openssl rsa -in key.rsa -pubout > key.rsa.pub
Перевірте щоб файл key.rsa
починався з рядка “—–BEGIN RSA PRIVATE KEY—–“, а key.rsa.pub
– з “—–BEGIN PUBLIC KEY—–“. І не переплутайте, а то логін не буде безпечним і взагалі довго дебажити доведеться.
Приватний ключ використовується для підписування документа, тобто документ зашифровується цим ключем і додається до відкритого як підпис. Документом буде JSON Web Token, і він буде містити зрозумілі комп’ютеру твердження на зразок “власник цього токена має право записувати дані на сервер до такого-то числа, підписано сервером”. Токен буде видаватись якщо ми передамо правильні логін і пароль на “/login”. Можна вважати приватний ключ печаткою, а публічний – зразком печатки для порівняння.
Щоб підписувати токени ключами нам знадобиться бібліотека “jwt-go”. Встановлюється командою go get github.com/dgrijalva/jwt-go
. Тепер нам треба трохи модифікувати функцію main()
і додати бібліотек:
import ( // крім вищенаписаного додати ще ці, знадобиться: "crypto/rsa" "github.com/dgrijalva/jwt-go" "time" ) func main() { initDB() err := loadKeys() // Завантаження ключів if err != nil { fmt.Println(err) return } http.HandleFunc("/login", loginHandler) // тут будемо видавати токен http.HandleFunc("/", handler) // а тут будемо іноді перевіряти fmt.Println("Listening at 8080") http.ListenAndServe(":8080", nil) } // В цих змінних зберігатимемо ключі var PublicKey *rsa.PublicKey var PrivateKey *rsa.PrivateKey // Ця функція просто прочитає і розпарсить ключі у змінні вище, нічого цікавого func loadKeys() error { pk, err := ioutil.ReadFile("./key.rsa") if err != nil { return err } PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(pk) if err != nil { return err } pk, err = ioutil.ReadFile("./key.rsa.pub") if err != nil { return err } PublicKey, err = jwt.ParseRSAPublicKeyFromPEM(pk) return err } // Структура в яку ми розпакуємо запит до /login type UserCredentials struct { Login string `json:"login"` Password string `json:"password"` } // Якщо отримує запит з правильними логіном і паролем повертає нам // підписаний токен для доступу до запису в "БД" func loginHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed) return } var user UserCredentials var err error bodybytes, err := ioutil.ReadAll(r.Body) err = json.Unmarshal(bodybytes, &user) if errorHandler(w, err, http.StatusUnprocessableEntity) { return } if (user.Login != "LOGIN") || (user.Password != "PASSWORD") { errorHandler(w, fmt.Errorf("Bad credentials"), http.StatusForbidden) return } // якщо всі перевірки пройдено - згенерувати токен tokenString, err := getJWT() if errorHandler(w, err, http.StatusInternalServerError) { return } msg, _ := json.Marshal(map[string]string{ "token": tokenString, }) w.Write(msg) } // Функція що повертає нам текст токена, підписаний приватним ключем func getJWT() (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ // тут записуємо дозвіл на post (взагалі, можна що завгодно записувати) "allow": "post", // пишемо що токен дійсний пів години "exp": time.Now().Add(time.Minute * 30).Unix(), }) return token.SignedString(PrivateKey) }
Пробуємо дістати токен:
curl -X POST http://localhost:8080/login -d '{"login": "LOGIN", "password": "PASSWORD"}' # Дає {"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGxvdyI6InBvc3QiLCJleHAiOjE1MTI5NDE0NjF9.nKZOXO-sTEsmzVRFI3dfSl0MbO9v-fYBRZkS-GzpmlGgwho5NgxUASax0VnZX4v0mLKlI0Jt2bncDn4jZA1TZsmMTCkAetPslcrkWhIt5XaLASmnyzm_-VTCSoSSDtFydpr3pAceEfg41tuBeukhokh-focDGDSZLQTA4_MeY00"} curl -X POST http://localhost:8080/login -d '{"login": "LOGIN", "password": "WRONG"}' # Дає {"error":"Bad credentials"}
Бачимо що токен – це три base64 стрічки записані через крапку. Є веб-сервіс що дозволяє розкодувати і подивитись що там. Перша – заголовок, описує формат токена:
{ "alg": "RS256", "typ": "JWT" }
Тіло – описує власне якісь твердження, в нашому випадку те що ми дозволяємо деякий час робити POST запити:
{ "allow": "post", "exp": 1512941461 }
Тепер треба оновити основний handler
, аби він перевіряв наявність і правильність токена.
func handler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { data, err := json.Marshal(log) if errorHandler(w, err, http.StatusInternalServerError) { return } w.Write(data) } else if r.Method == "POST" { // Цього разу перед тим як робити POST allow, err := isPostAllowed(r) // перевіряємо чи можна if errorHandler(w, err, http.StatusInternalServerError) { return } if !allow { // І якщо не можна - повертаємо помилку errorHandler(w, fmt.Errorf("Access denied"), http.StatusForbidden) } bodybytes, err := ioutil.ReadAll(r.Body) if errorHandler(w, err, http.StatusInternalServerError) { return } log = append(log, string(bodybytes)) w.Write(bodybytes) } else { errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed) } } // Перевірка дозволів func isPostAllowed(r *http.Request) (bool, error) { bearer := r.Header.Get("Authorization") // Отримуємо заголовок Authorization prefixLen := len("Bearer ") if len(bearer) <= prefixLen { return false, fmt.Errorf("Authorization header is too short") } // Пробуємо парсити токен. Два аргументи - токен і функція що повертає потрібний ключ token, err := jwt.Parse(bearer[prefixLen:], func(token *jwt.Token) (interface{}, error) { return PublicKey, nil }) if err != nil { return false, err } // Якщо вийшло розпарсити, токен валідний, то дістаємо твердження if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { allow, ok := claims["allow"].(string) // дивимось чи написано щось про "allow" if ok && strings.Contains(allow, "post") { // і чи є там "post" return true, nil } return false, fmt.Errorf("Token does not have claim to allow this action") } else { return false, fmt.Errorf("Token is invalid") } return true, nil }
Тепер попробуємо щось запостити без токена і з ним:
curl -X POST http://localhost:8080/ -d "it works!" # Дає {"error":"Authorization header is too short"} curl -X POST http://localhost:8080/ -d "it works!" -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGxvdyI6InBvc3QiLCJleHAiOjE1MTI5NDE0NjF9.nKZOXO-sTEsmzVRFI3dfSl0MbO9v-fYBRZkS-GzpmlGgwho5NgxUASax0VnZX4v0mLKlI0Jt2bncDn4jZA1TZsmMTCkAetPslcrkWhIt5XaLASmnyzm_-VTCSoSSDtFydpr3pAceEfg41tuBeukhokh-focDGDSZLQTA4_MeY00" # Дає it works
Працює! Отож, бачимо що JWT це дуже навіть просто, якщо не рахувати заморочок з дивними способами виклику функцій бібліотеки – виписуємо довідку що щось можна ставимо на неї печатку, а в іншому місці це перевіряємо.
Filed under: Кодерство, Павутина Tagged: go