package
0.0.0-20241212234551-d1328d747c9a
Repository: https://github.com/leetmansup/techsoftwarecreating.git
Documentation: pkg.go.dev

# README

Практическая работа №8. Аутентификация и авторизация в REST API

0. Установка зависимостей (JWT-Go)

Установим библиотеку для работы с JWT:

    go get -u github.com/dgrijalva/jwt-go

1. Подготовка переменных и структур

Создадим 3 структуры:

  • Пользователь
    type User struct {
        Username string `json:"username"`
        Password string `json:"password"`
        Role     string `json:"role"`
    }
    
  • Требования для Access JWT
    type Claims struct {
        Username string `json:"username"`
        Role     string `json:"role"`
        jwt.StandardClaims
    }
    
  • Требования для Refresh JWT
    type RefreshClaims struct {
        Username string `json:"username"`
        jwt.StandardClaims
    }
    

Создадим переменную для хранения "секрета" генерации токенов:

var jwtKey = []byte("secret")

Сымитируем базу данных, создав срез тестовых пользователей:

var users = []User{
	{
		Username: "admin",
		Password: "adminPassword",
		Role:     "admin",
	},
	{
		Username: "user1",
		Password: "password1",
		Role:     "user",
	},
	{
		Username: "user2",
		Password: "password2",
		Role:     "user",
	},
}

2. Реализация функций

Напишем функцию, которая генерирует Access и Refresh токены:

func generateTokens(username, role string) (accessToken, refreshToken string, err error) {
	// Генерация Access Token (короткоживущего токена)
	accessTokenExpiration := time.Now().Add(5 * time.Minute)
	accessClaims := &Claims{
		Username: username,
		Role:     role,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: accessTokenExpiration.Unix(),
		},
	}
	accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
	accessToken, err = accessTokenObj.SignedString(jwtKey)
	if err != nil {
		return "", "", err
	}

	// Генерация Refresh Token (долгоживущего токена)
	refreshTokenExpiration := time.Now().Add(7 * 24 * time.Hour)
	refreshClaims := &RefreshClaims{
		Username: username,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: refreshTokenExpiration.Unix(),
		},
	}
	refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
	refreshToken, err = refreshTokenObj.SignedString(jwtKey)
	return
}

Напишем функцию, реализующую логику регистрации в системе:

func register(c *gin.Context) {
	var newUser User
	if err := c.BindJSON(&newUser); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "invalid request"})
		return
	}

	for _, user := range users {
		if user.Username == newUser.Username {
			c.JSON(http.StatusConflict, gin.H{"message": "user already exists"})
			return
		}
	}

	users = append(users, newUser)
	c.JSON(http.StatusCreated, gin.H{"message": "user registered successfully"})
}

Напишем функцию, реализующую логику входа в систему:

func login(c *gin.Context) {
	var credentials User
	if err := c.BindJSON(&credentials); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "invalid request"})
		return
	}

	var role string
	for _, user := range users {
		if user.Username == credentials.Username && user.Password == credentials.Password {
			role = user.Role
			break
		}
	}

	if role == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
		return
	}

	accessToken, refreshToken, err := generateTokens(credentials.Username, role)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"message": "could not generate token"})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"access_token":  accessToken,
		"refresh_token": refreshToken,
	})
}

Напишем функцию, реализующую логику обновления Access токена:

func refresh(c *gin.Context) {
	var requestBody struct {
		RefreshToken string `json:"refresh_token"`
	}

	if err := c.BindJSON(&requestBody); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "invalid request"})
		return
	}

	refreshToken := requestBody.RefreshToken
	claims := &RefreshClaims{}
	token, err := jwt.ParseWithClaims(refreshToken, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtKey, nil
	})

	if err != nil || !token.Valid {
		c.JSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
		return
	}

	// Генерация нового Access Token
	var role string
	for _, user := range users {
		if user.Username == claims.Username {
			role = user.Role
			break
		}
	}

	if role == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
		return
	}

	accessTokenExpiration := time.Now().Add(5 * time.Minute)
	accessClaims := &Claims{
		Username: claims.Username,
		Role:     role,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: accessTokenExpiration.Unix(),
		},
	}
	accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
	newAccessToken, err := accessTokenObj.SignedString(jwtKey)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"message": "could not generate token"})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"access_token": newAccessToken,
	})
}

Напишем промежуточную функцию (мидлвар), реализующую логику авторизации по JWT:

func authMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		tokenString := c.GetHeader("Authorization")
		claims := &Claims{}
		token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
			return jwtKey, nil
		})
		if err != nil || !token.Valid {
			c.JSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
			c.Abort()
			return
		}
		c.Set("claims", claims)
		c.Next()
	}
}

Напишем промежуточную функцию (мидлвар), реализующую логику проверки соответствия прав пользователя на совершение запроса:

func roleMiddleware(requiredRole string) gin.HandlerFunc {
	return func(c *gin.Context) {
		claims, exists := c.Get("claims")
		if !exists {
			c.JSON(http.StatusForbidden, gin.H{"message": "forbidden"})
			c.Abort()
			return
		}

		userClaims := claims.(*Claims)
		if userClaims.Role != requiredRole {
			c.JSON(http.StatusForbidden, gin.H{"message": "forbidden"})
			c.Abort()
			return
		}
		c.Next()
	}
}

Обновим функцию main:

func main() {
	router := gin.Default()

	router.POST("/register", register) // Регистрация
	router.POST("/login", login)       // Вход
	router.POST("/refresh", refresh)   // Обновление Access Token

	protected := router.Group("/")
	protected.Use(authMiddleware())
	{
		protected.GET("/goods", getGoods)
		protected.GET("/good/:id", getGood)
		protected.POST("/goods", roleMiddleware("admin"), createGood)
		protected.PATCH("/good/:id", roleMiddleware("admin"), updateGood)
		protected.DELETE("/good/:id", roleMiddleware("admin"), deleteGood)

	}

	_ = router.Run(":8888")
}

3. Результаты

Регистрация пользователя в системе

Отправляем POST запрос на http://localhost:8888/register c body со следующим содержимым (логин, пароль и роль регистрируемого пользователя):

{
    "username": "newuser",
    "password": "newpassword",
    "role": "user"
}

Получаем ответ (сообщение об успешной регистрации):

{
    "message": "user registered successfully"
}

Скриншот: alt text

Вход администратора в систему

Отправляем POST запрос на http://localhost:8888/login c body со следующим содержимым (логин и пароль администратора):

{
  "username": "admin",
  "password": "adminPassword"
}

Получаем ответ (access и refresh токены пользователя admin):

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzMxMzE4NDc5fQ.X_3mI4DGda4nonKiB5jEI0__jP-Q0CD-B6PUwh76avo",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzMxOTIyOTc5fQ.W6yaA80-SBL-LekYF-wuoIRMUVKtnwU9sA42AGh1lvs"
}

Скриншот: alt text

Вход рядового пользователя в систему

Отправляем POST запрос на http://localhost:8888/login c body со следующим содержимым (логин и пароль рядового пользователя):

{
  "username": "user1",
  "password": "password1"
}

Получаем ответ (access и refresh токены пользователя user1):

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MzEzMTg1OTh9.Sno9GU6NGWSIvjr1UWTwOeH8siJU6Y_JQw8daVpuCgI",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNzMxOTIzMDk4fQ.nw_BpTS9cjV6kKiQIb1ByTXRIQm7J7LVlweYnq6neP8"
}

Скриншот: alt text

Обновление Access Token (на примере токена рядового пользователя user1)

Отправляем POST запрос на http://localhost:8888/refresh c body со следующим содержимым (refresh token пользователя user1):

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNzMxOTIzMDk4fQ.nw_BpTS9cjV6kKiQIb1ByTXRIQm7J7LVlweYnq6neP8"
}

Получаем ответ (новый access token пользователя user1):

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MzEzMTg4MDh9.Ra-EzAVP5lpkFpSP3nXMzJJgaHrwrEbnidTacjSUACc"
}

Скриншот: alt text

Проверка доступности маршрутов для рядового пользователя (на примере получения списка товаров и добавления нового товара)

Получение списка товаров

Данный маршрут должен быть доступен рядовому пользователю.

Отправляем GET запрос на http://localhost:8888/goods cо следующим header (JWT рядового пользователя user1):

Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MzEzMTg4MDh9.Ra-EzAVP5lpkFpSP3nXMzJJgaHrwrEbnidTacjSUACc

Получаем ответ (маршрут доступен, получили список товаров):

[
    {
        "id": "1",
        "name": "Стол",
        "description": "Обычный деревянный стол",
        "price": 10000
    },
    {
        "id": "2",
        "name": "Стул",
        "description": "Обычный железный стул",
        "price": 3000
    },
    {
        "id": "3",
        "name": "Ковёр",
        "description": "Красный совковый ковёр",
        "price": 5000
    }
]

Скриншот: alt text

Добавление нового товара

Данный маршрут должен быть запрещён рядовому пользователю.

Отправляем POST запрос на http://localhost:8888/goods cо следующим header (JWT рядового пользователя user1):

Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MzEzMTg4MDh9.Ra-EzAVP5lpkFpSP3nXMzJJgaHrwrEbnidTacjSUACc

и со следующим body (данные добавляемого товара):

{
    "id": "4",
    "name": "ПОЛ",
    "description": "ПАРКЕТ",
    "price": 10
}

Получаем ответ (доступ запрещён):

{
    "message": "forbidden"
}

Скриншот: alt text

Проверка доступности маршрутов для администратора (на примере получения списка товаров и добавления нового товара)

Получение списка товаров

Данный маршрут должен быть доступен рядовому пользователю.

Отправляем GET запрос на http://localhost:8888/goods cо следующим header (JWT администратора admin):

Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzMxMzE5Nzk5fQ.QVO5hf1RXr1l35rzBFeSZNq66CghmX4H_YO2cHKH6BQ

Получаем ответ (маршрут доступен, получили список товаров):

[
    {
        "id": "1",
        "name": "Стол",
        "description": "Обычный деревянный стол",
        "price": 10000
    },
    {
        "id": "2",
        "name": "Стул",
        "description": "Обычный железный стул",
        "price": 3000
    },
    {
        "id": "3",
        "name": "Ковёр",
        "description": "Красный совковый ковёр",
        "price": 5000
    }
]

Скриншот: alt text

Добавление нового товара

Данный маршрут должен быть доступен администратору.

Отправляем POST запрос на http://localhost:8888/goods cо следующим header (JWT администратора admin):

Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzMxMzE5Nzk5fQ.QVO5hf1RXr1l35rzBFeSZNq66CghmX4H_YO2cHKH6BQ

и со следующим body (данные добавляемого товара):

{
    "id": "5",
    "name": "НОВЫЙ_ПОЛ",
    "description": "СУПЕР_ПАРКЕТ",
    "price": 20
}

Получаем ответ (маршрут доступен, товар был добавлен):

{
    "id": "5",
    "name": "НОВЫЙ_ПОЛ",
    "description": "СУПЕР_ПАРКЕТ",
    "price": 20
}

Скриншот: alt text

Проверка добавления товара (товар успешно добавлен в систему): alt text

# Structs

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author