Щоб щось зрозуміти іноді легше писати ніж читати (В мене була потреба зрозуміти як працює 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