# 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"
}
Скриншот:
Вход администратора в систему
Отправляем 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"
}
Скриншот:
Вход рядового пользователя в систему
Отправляем 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"
}
Скриншот:
Обновление 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"
}
Скриншот:
Проверка доступности маршрутов для рядового пользователя (на примере получения списка товаров и добавления нового товара)
Получение списка товаров
Данный маршрут должен быть доступен рядовому пользователю.
Отправляем 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
}
]
Скриншот:
Добавление нового товара
Данный маршрут должен быть запрещён рядовому пользователю.
Отправляем POST
запрос на http://localhost:8888/goods
cо следующим header
(JWT рядового пользователя user1
):
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MzEzMTg4MDh9.Ra-EzAVP5lpkFpSP3nXMzJJgaHrwrEbnidTacjSUACc
и со следующим body
(данные добавляемого товара):
{
"id": "4",
"name": "ПОЛ",
"description": "ПАРКЕТ",
"price": 10
}
Получаем ответ (доступ запрещён):
{
"message": "forbidden"
}
Скриншот:
Проверка доступности маршрутов для администратора (на примере получения списка товаров и добавления нового товара)
Получение списка товаров
Данный маршрут должен быть доступен рядовому пользователю.
Отправляем 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
}
]
Скриншот:
Добавление нового товара
Данный маршрут должен быть доступен администратору.
Отправляем 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
}
Скриншот:
Проверка добавления товара (товар успешно добавлен в систему):