Эта статья изначально была размещена в Моем блоге
TL; DR: В этом руководстве я покажу вам, как легко создать веб-приложение с помощью Go и Gin framework и добавить в него аутентификацию. Загляните в репозиторий Github, чтобы найти код, который мы собираемся написать.
Gin - это высокопроизводительная микросхема. Он предоставляет очень минималистичную структуру, которая включает в себя только самые важные функции, библиотеки и функции, необходимые для создания веб-приложений и микросервисов. Это упрощает построение конвейера обработки запросов из модульных многоразовых частей. Он делает это, позволяя вам писать промежуточное программное обеспечение, которое можно подключить к одному или нескольким обработчикам запросов или группам обработчиков запросов.
Особенности джина
Gin - это быстрый, простой, но полнофункциональный и очень эффективный веб-фреймворк для Go. Ознакомьтесь с некоторыми из приведенных ниже функций, которые делают его достойным фреймворком для вашего следующего проекта Golang.
- Скорость. Джин создан для скорости. Платформа предлагает маршрутизацию на основе дерева Radix и небольшой объем памяти. Никакого отражения. Прогнозируемая производительность API.
- Без сбоев: Gin может обнаруживать сбои или паники во время выполнения и может восстанавливаться после них. Таким образом ваше приложение всегда будет доступно.
- Маршрутизация: Gin предоставляет интерфейс маршрутизации, который позволяет вам указать, как должны выглядеть маршруты вашего веб-приложения или API.
- Проверка JSON. Gin может легко анализировать и проверять запросы JSON, проверяя наличие требуемых значений.
- Управление ошибками. Gin обеспечивает удобный способ сбора всех ошибок, возникших во время HTTP-запроса. В конце концов, промежуточное ПО может записывать их в файл журнала или в базу данных и отправлять по сети.
- Встроенный рендеринг: Gin предоставляет простой в использовании API для рендеринга JSON, XML и HTML.
Предпосылки
Чтобы следовать этому руководству, вам потребуется установить Go на вашем компьютере, веб-браузер для просмотра приложения и командную строку для выполнения команд сборки.
Go или, как его обычно называют, Golang - это язык программирования, разработанный Google для создания современного программного обеспечения. Go - это язык, предназначенный для быстрого и эффективного выполнения задач. Ключевые преимущества Go:
- Строго типизированный и сборщик мусора
- Молниеносно быстрое время компиляции
- Встроенный параллелизм
- Обширная стандартная библиотека
Перейдите в раздел загрузок на веб-сайте Go, чтобы запустить Go на вашем компьютере.
Создание приложения с помощью Gin
Мы создадим простое приложение со списком анекдотов с помощью Джина. Наше приложение перечислит несколько глупых отцовских шуток. Мы собираемся добавить к нему аутентификацию, чтобы все пользователи, вошедшие в систему, имели право ставить лайки и просматривать анекдоты.
Это позволит нам проиллюстрировать, как Gin можно использовать для разработки веб-приложений и / или API.
Мы будем использовать следующие функции, предлагаемые Gin:
- ПО промежуточного слоя
- Маршрутизация
- Группировка маршрутов
На старт, внимание, марш
Мы напишем все наше приложение Go в main.go
файл. Поскольку это небольшое приложение, его будет легко создать, используя всего go run
из терминала.
Мы создадим новый каталог golang-gin
в нашей рабочей области Go, а затем main.go
файл в нем:
$ mkdir -p $GOPATH/src/github.com/user/golang-gin
$ cd $GOPATH/src/github.com/user/golang-gin
$ touch main.go
Содержание файла main.go
:
package main
import ( "net/http"
"github.com/gin-gonic/contrib/static" "github.com/gin-gonic/gin" )
func main() { // Set the router as the default one shipped with Gin router := gin.Default()
// Serve frontend static files router.Use(static.Serve("/", static.LocalFile("./views", true)))
// Setup route group for the API api := router.Group("/api") { api.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H { "message": "pong", }) }) }
// Start and run the server router.Run(":3000") }
Нам нужно будет создать еще несколько каталогов для наших статических файлов. В том же каталоге, что и файл main.go
, давайте создадим папку views
. В папке views
создайте папку js
и файл index.html
в ней.
Файл index.html
пока будет очень простым:
<!DOCTYPE html> <html> <head> <title>Jokeish App</title> </head>
<body> <h1>Welcome to the Jokeish App</h1> </body> </html>
Прежде чем мы протестируем то, что у нас есть, давайте установим добавленные зависимости:
$ go get -u github.com/gin-gonic/gin
$ go get -u github.com/gin-gonic/contrib/static
Чтобы увидеть, что работает, нам нужно запустить наш сервер, запустив go run main.go
.
После запуска приложения перейдите к http://localhost:3000
в своем браузере. Если все прошло успешно, вы должны увидеть текст заголовка уровня 1 Добро пожаловать в приложение Jokeish.
Определение API
Давайте добавим еще немного кода в наш main.go
файл для наших определений API. Мы обновим нашу main
функцию двумя маршрутами /jokes/
и /jokes/like/:jokeID
в группу маршрутов /api/
.
func main() { // ... leave the code above untouched...
// Our API will consit of just two routes // /jokes - which will retrieve a list of jokes a user can see // /jokes/like/:jokeID - which will capture likes sent to a particular joke api.GET("/jokes", JokeHandler) api.POST("/jokes/like/:jokeID", LikeJoke) }
// JokeHandler retrieves a list of available jokes func JokeHandler(c *gin.Context) { c.Header("Content-Type", "application/json") c.JSON(http.StatusOK, gin.H { "message":"Jokes handler not implemented yet", }) }
// LikeJoke increments the likes of a particular joke Item func LikeJoke(c *gin.Context) { c.Header("Content-Type", "application/json") c.JSON(http.StatusOK, gin.H { "message":"LikeJoke handler not implemented yet", }) }
Содержимое файла main.go
должно выглядеть так:
package main
import ( "net/http"
"github.com/gin-gonic/contrib/static" "github.com/gin-gonic/gin" )
func main() { // Set the router as the default one shipped with Gin router := gin.Default()
// Serve frontend static files router.Use(static.Serve("/", static.LocalFile("./views", true)))
// Setup route group for the API api := router.Group("/api") { api.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H { "message": "pong", }) }) } // Our API will consit of just two routes // /jokes - which will retrieve a list of jokes a user can see // /jokes/like/:jokeID - which will capture likes sent to a particular joke api.GET("/jokes", JokeHandler) api.POST("/jokes/like/:jokeID", LikeJoke)
// Start and run the server router.Run(":3000") }
// JokeHandler retrieves a list of available jokes func JokeHandler(c *gin.Context) { c.Header("Content-Type", "application/json") c.JSON(http.StatusOK, gin.H { "message":"Jokes handler not implemented yet", }) }
// LikeJoke increments the likes of a particular joke Item func LikeJoke(c *gin.Context) { c.Header("Content-Type", "application/json") c.JSON(http.StatusOK, gin.H { "message":"LikeJoke handler not implemented yet", }) }
Давайте снова запустим наше приложение go run main.go
и получим доступ к нашим маршрутам. http://localhost:3000/api/jokes
вернет ответ заголовка 200 OK
с сообщением jokes handler not implemented yet
. Запрос POST к http://localhost:3000/api/jokes/like/1
возвращает заголовок 200 OK
и сообщение Likejoke handler not implemented yet
.
Данные анекдотов
Поскольку у нас уже есть набор определений маршрутов, который выполняет только одно действие (возвращает ответ JSON), мы немного оживим нашу кодовую базу, добавив в нее еще немного кода.
// ... leave the code above untouched...
// Let's create our Jokes struct. This will contain information about a Joke
// Joke contains information about a single Joke type Joke struct { ID int `json:"id" binding:"required"` Likes int `json:"likes"` Joke string `json:"joke" binding:"required"` }
// We'll create a list of jokes var jokes = []Joke{ Joke{1, 0, "Did you hear about the restaurant on the moon? Great food, no atmosphere."}, Joke{2, 0, "What do you call a fake noodle? An Impasta."}, Joke{3, 0, "How many apples grow on a tree? All of them."}, Joke{4, 0, "Want to hear a joke about paper? Nevermind it's tearable."}, Joke{5, 0, "I just watched a program about beavers. It was the best dam program I've ever seen."}, Joke{6, 0, "Why did the coffee file a police report? It got mugged."}, Joke{7, 0, "How does a penguin build it's house? Igloos it together."}, }
func main() { // ... leave this block untouched... }
// JokeHandler retrieves a list of available jokes func JokeHandler(c *gin.Context) { c.Header("Content-Type", "application/json") c.JSON(http.StatusOK, jokes) }
// LikeJoke increments the likes of a particular joke Item func LikeJoke(c *gin.Context) { // confirm Joke ID sent is valid // remember to import the `strconv` package if jokeid, err := strconv.Atoi(c.Param("jokeID")); err == nil { // find joke, and increment likes for i := 0; i < len(jokes); i++ { if jokes[i].ID == jokeid { jokes[i].Likes += 1 } }
// return a pointer to the updated jokes list c.JSON(http.StatusOK, &jokes) } else { // Joke ID is invalid c.AbortWithStatus(http.StatusNotFound) } }
// NB: Replace the JokeHandler and LikeJoke functions in the previous version to the ones above
Когда наш код выглядит хорошо, давайте продолжим и протестируем наш API. Мы можем протестировать с cURL
или postman
, а затем отправить GET
запрос http://localhost:3000/jokes
, чтобы получить полный список шуток, и POST
запрос http://localhost:3000/jokes/like/{jokeid}
, чтобы увеличить количество подобных шуток.
$ curl http://localhost:3000/api/jokes
$ curl -X POST http://localhost:3000/api/jokes/like/4
Создание пользовательского интерфейса (React)
У нас есть API, поэтому давайте создадим интерфейс для представления данных из нашего API. Для этого мы будем использовать React. Мы не будем углубляться в React, так как это выйдет за рамки данного руководства. Если вам нужно узнать больше о React, загляните в официальный учебник. Вы можете реализовать пользовательский интерфейс с любой удобной для вас интерфейсом.
Настраивать
Мы отредактируем файл index.html
, чтобы добавить внешние библиотеки, необходимые для запуска React. Затем нам нужно создать файл app.jsx
в каталоге views/js
, который будет содержать наш код React.
Наш index.html
файл должен выглядеть так:
<!DOCTYPE html> <html>
<head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>Jokeish App</title> <script src="http://code.jquery.com/jquery-2.1.4.min.js"></script> <script src="https://cdn.auth0.com/js/auth0/9.0/auth0.min.js"></script> <script type="application/javascript" src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script> <script type="application/javascript" src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script> <script type="application/javascript" src="https://unpkg.com/[email protected]/babel.js"></script> <script type="text/babel" src="js/app.jsx"></script> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"> </head>
<body> <div id="app"></div> </body>
</html>
Сборка наших компонентов
В React представления разбиты на компоненты. Нам нужно будет собрать несколько компонентов:
App
компонент в качестве основной записи, запускающей приложениеHome
компонент, который будет сталкиваться с пользователями, не вошедшими в системуLoggedIn
компонент с содержимым, видимым только авторизованным пользователям- и компонент
Joke
для отображения списка шуток.
Мы запишем все эти компоненты в файл app.jsx
.
Компонент приложения
Этот компонент загружает все наше приложение React. Он решает, какой компонент показывать, в зависимости от того, аутентифицирован пользователь или нет. Мы начнем с его базы, а позже обновим ее, добавив больше функций.
class App extends React.Component {
render() {
if (this.loggedIn) {
return (<LoggedIn />);
} else {
return (<Home />);
}
}
}
Компонент Home
Этот компонент отображается для пользователей, не вошедших в систему, вместе с кнопкой, которая открывает размещенный экран блокировки, где они могут зарегистрироваться или войти в систему. Мы добавим эту функцию позже.
class Home extends React.Component {
render() {
return (
<div className="container">
<div className="col-xs-8 col-xs-offset-2 jumbotron text-center">
<h1>Jokeish</h1>
<p>A load of Dad jokes XD</p>
<p>Sign in to get access </p>
<a onClick={this.authenticate} className="btn btn-primary btn-lg btn-login btn-block">Sign In</a>
</div>
</div>
)
}
}
Компонент LoggedIn
Этот компонент отображается, когда пользователь аутентифицирован. Он хранит в своем state
массив шуток, который заполняется, когда компонент монтируется.
class LoggedIn extends React.Component { constructor(props) { super(props); this.state = { jokes: [] } }
render() { return ( <div className="container"> <div className="col-lg-12"> <br /> <span className="pull-right"><a onClick={this.logout}>Log out</a></span> <h2>Jokeish</h2> <p>Let's feed you with some funny Jokes!!!</p> <div className="row"> {this.state.jokes.map(function(joke, i){ return (<Joke key={i} joke={joke} />); })} </div> </div> </div> ) } }
Компонент "Шутка"
Компонент Joke
будет содержать информацию о каждом элементе ответа на шутку, который будет отображаться.
class Joke extends React.Component { constructor(props) { super(props); this.state = { liked: "" } this.like = this.like.bind(this); }
like() { // ... we'll add this block later }
render() { return ( <div className="col-xs-4"> <div className="panel panel-default"> <div className="panel-heading">#{this.props.joke.id} <span className="pull-right">{this.state.liked}</span></div> <div className="panel-body"> {this.props.joke.joke} </div> <div className="panel-footer"> {this.props.joke.likes} Likes <a onClick={this.like} className="btn btn-default"> <span className="glyphicon glyphicon-thumbs-up"></span> </a> </div> </div> </div> ) } }
Мы написали наши компоненты, поэтому теперь давайте скажем React, где рендерить приложение. Мы добавим приведенный ниже блок кода в конец нашего app.jsx
файла.
ReactDOM.render(<App />, document.getElementById('app'));
Давайте перезапустим наш сервер Go go run main.go
и перейдем к URL-адресу нашего приложения http://localhost:3000/
. Вы увидите, что компонент Home
визуализируется.
Защита нашего приложения для шуток с помощью Auth0
Auth0 выдает веб-токены JSON при каждом входе в систему для ваших пользователей. Это означает, что у вас может быть надежная инфраструктура идентификации, включая единый вход, управление пользователями, поддержку поставщиков социальной идентификации (Facebook, Github, Twitter и т. Д.), Поставщиков корпоративной идентификации (Active Directory, LDAP, SAML и т. д.) и вашу собственную базу данных пользователей с помощью всего нескольких строк кода.
Мы можем легко настроить аутентификацию в нашем приложении GIN с помощью Auth0. Чтобы следовать этой части, вам потребуется учетная запись. Если у вас еще нет учетной записи Auth0, зарегистрируйтесь.
Заявление об ограничении ответственности: это не спонсорский контент.
Создание клиента API
Наши токены будут сгенерированы с помощью Auth0, поэтому нам нужно создать API и клиент из нашей панели инструментов Auth0. Опять же, если вы еще этого не сделали, зарегистрируйтесь для учетной записи Auth0.
Чтобы создать новый API, перейдите в раздел API на панели инструментов и нажмите кнопку Создать API.
Выберите имя API и идентификатор. Идентификатор будет аудиторией для промежуточного программного обеспечения. Алгоритм подписи должен быть RS256.
Чтобы создать нового клиента, перейдите в раздел клиентов на панели управления и нажмите кнопку Создать клиента. Выберите тип Regular Web Applications
.
После создания клиента обратите внимание на client_id
и client_secret
, так как они нам понадобятся позже.
Мы должны добавить учетные данные, необходимые для нашего API, в переменную среды. В корневом каталоге создайте новый файл .env
и добавьте к нему следующее с деталями из панели инструментов Auth0:
export AUTH0_API_CLIENT_SECRET=""
export AUTH0_CLIENT_ID=""
export AUTH0_DOMAIN="yourdomain.auth0.com"
export AUTH0_API_AUDIENCE=""
Защита наших конечных точек API
В настоящее время наш API открыт для всего мира. Нам необходимо защитить наши конечные точки, чтобы только авторизованные пользователи могли получить к ним доступ.
Мы собираемся использовать промежуточное ПО JWT для проверки действительного веб-токена JSON для каждого запроса, попадающего в наши конечные точки.
Давайте создадим наше промежуточное ПО:
// ...
var jwtMiddleWare *jwtmiddleware.JWTMiddleware
func main() { jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{ ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { aud := os.Getenv("AUTH0_API_AUDIENCE") checkAudience := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false) if !checkAudience { return token, errors.New("Invalid audience.") } // verify iss claim iss := os.Getenv("AUTH0_DOMAIN") checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false) if !checkIss { return token, errors.New("Invalid issuer.") }
cert, err := getPemCert(token) if err != nil { log.Fatalf("could not get cert: %+v", err) }
result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) return result, nil }, SigningMethod: jwt.SigningMethodRS256, })
// register our actual jwtMiddleware jwtMiddleWare = jwtMiddleware
// ... the rest of the code below this function doesn't change yet }
// authMiddleware intercepts the requests, and check for a valid jwt token func authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Get the client secret key err := jwtMiddleWare.CheckJWT(c.Writer, c.Request) if err != nil { // Token not found fmt.Println(err) c.Abort() c.Writer.WriteHeader(http.StatusUnauthorized) c.Writer.Write([]byte("Unauthorized")) return } } }
В приведенном выше коде у нас есть новая переменная jwtMiddleWare
, которая инициализируется функцией main
. Он используется в средней функции authMiddleware
.
Если вы заметили, мы извлекаем наши учетные данные на стороне сервера из переменной среды (один из принципов 12-факторного приложения). Наше промежуточное программное обеспечение проверяет и получает токен из запроса и вызывает метод jwtMiddleWare.CheckJWT
для проверки отправленного токена.
Также напишем функцию для возврата веб-ключей JSON:
// ... the code above is untouched...
// Jwks stores a slice of JSON Web Keys type Jwks struct { Keys []JSONWebKeys `json:"keys"` }
type JSONWebKeys struct { Kty string `json:"kty"` Kid string `json:"kid"` Use string `json:"use"` N string `json:"n"` E string `json:"e"` X5c []string `json:"x5c"` }
func main() { // ... the code in this method is untouched... }
func getPemCert(token *jwt.Token) (string, error) { cert := "" resp, err := http.Get(os.Getenv("AUTH0_DOMAIN") + ".well-known/jwks.json") if err != nil { return cert, err } defer resp.Body.Close()
var jwks = Jwks{} err = json.NewDecoder(resp.Body).Decode(&jwks)
if err != nil { return cert, err }
x5c := jwks.Keys[0].X5c for k, v := range x5c { if token.Header["kid"] == jwks.Keys[k].Kid { cert = "-----BEGIN CERTIFICATE-----\n" + v + "\n-----END CERTIFICATE-----" } }
if cert == "" { return cert, errors.New("unable to find appropriate key.") }
return cert, nil }
Использование промежуточного программного обеспечения JWT
Использовать промежуточное ПО очень просто. Мы просто передаем его как параметр в определение наших маршрутов.
...
api.GET("/jokes", authMiddleware(), JokeHandler) api.POST("/jokes/like/:jokeID", authMiddleware(), LikeJoke)
...
Наш main.go
файл должен выглядеть так:
package main
import ( "encoding/json" "errors" "fmt" "log" "net/http" "os" "strconv"
jwtmiddleware "github.com/auth0/go-jwt-middleware" jwt "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/contrib/static" "github.com/gin-gonic/gin" )
type Response struct { Message string `json:"message"` }
type Jwks struct { Keys []JSONWebKeys `json:"keys"` }
type JSONWebKeys struct { Kty string `json:"kty"` Kid string `json:"kid"` Use string `json:"use"` N string `json:"n"` E string `json:"e"` X5c []string `json:"x5c"` }
type Joke struct { ID int `json:"id" binding:"required"` Likes int `json:"likes"` Joke string `json:"joke" binding:"required"` }
/** we'll create a list of jokes */ var jokes = []Joke{ Joke{1, 0, "Did you hear about the restaurant on the moon? Great food, no atmosphere."}, Joke{2, 0, "What do you call a fake noodle? An Impasta."}, Joke{3, 0, "How many apples grow on a tree? All of them."}, Joke{4, 0, "Want to hear a joke about paper? Nevermind it's tearable."}, Joke{5, 0, "I just watched a program about beavers. It was the best dam program I've ever seen."}, Joke{6, 0, "Why did the coffee file a police report? It got mugged."}, Joke{7, 0, "How does a penguin build it's house? Igloos it together."}, }
var jwtMiddleWare *jwtmiddleware.JWTMiddleware
func main() { jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{ ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { aud := os.Getenv("AUTH0_API_AUDIENCE") checkAudience := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false) if !checkAudience { return token, errors.New("Invalid audience.") } // verify iss claim iss := os.Getenv("AUTH0_DOMAIN") checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false) if !checkIss { return token, errors.New("Invalid issuer.") }
cert, err := getPemCert(token) if err != nil { log.Fatalf("could not get cert: %+v", err) }
result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) return result, nil }, SigningMethod: jwt.SigningMethodRS256, })
jwtMiddleWare = jwtMiddleware // Set the router as the default one shipped with Gin router := gin.Default()
// Serve the frontend router.Use(static.Serve("/", static.LocalFile("./views", true)))
api := router.Group("/api") { api.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) api.GET("/jokes", authMiddleware(), JokeHandler) api.POST("/jokes/like/:jokeID", authMiddleware(), LikeJoke) } // Start the app router.Run(":3000") }
func getPemCert(token *jwt.Token) (string, error) { cert := "" resp, err := http.Get(os.Getenv("AUTH0_DOMAIN") + ".well-known/jwks.json") if err != nil { return cert, err } defer resp.Body.Close()
var jwks = Jwks{} err = json.NewDecoder(resp.Body).Decode(&jwks)
if err != nil { return cert, err }
x5c := jwks.Keys[0].X5c for k, v := range x5c { if token.Header["kid"] == jwks.Keys[k].Kid { cert = "-----BEGIN CERTIFICATE-----\n" + v + "\n-----END CERTIFICATE-----" } }
if cert == "" { return cert, errors.New("unable to find appropriate key") }
return cert, nil }
// authMiddleware intercepts the requests, and check for a valid jwt token func authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Get the client secret key err := jwtMiddleWare.CheckJWT(c.Writer, c.Request) if err != nil { // Token not found fmt.Println(err) c.Abort() c.Writer.WriteHeader(http.StatusUnauthorized) c.Writer.Write([]byte("Unauthorized")) return } } }
// JokeHandler returns a list of jokes available (in memory) func JokeHandler(c *gin.Context) { c.Header("Content-Type", "application/json")
c.JSON(http.StatusOK, jokes) }
func LikeJoke(c *gin.Context) { // Check joke ID is valid if jokeid, err := strconv.Atoi(c.Param("jokeID")); err == nil { // find joke and increment likes for i := 0; i < len(jokes); i++ { if jokes[i].ID == jokeid { jokes[i].Likes = jokes[i].Likes + 1 } } c.JSON(http.StatusOK, &jokes) } else { // the jokes ID is invalid c.AbortWithStatus(http.StatusNotFound) } }
Установим jwtmiddleware
библиотеки:
$ go get -u github.com/auth0/go-jwt-middleware
$ go get -u github.com/dgrijalva/jwt-go
Давайте создадим файл среды и перезапустим сервер приложений:
$ source .env
$ go run main.go
Теперь, если мы попытаемся получить доступ к любой из конечных точек, мы столкнемся с 401 Unauthorized
ошибкой. Это потому, что нам нужно отправить токен с запросом.
Войти с Auth0 и React
Давайте внедрим систему входа в систему, чтобы пользователи могли входить в систему или создавать учетные записи и получать доступ к нашим шуткам. Мы добавим в наш app.jsx
файл следующие учетные данные Auth0:
AUTH0_CLIENT_ID
AUTH0_DOMAIN
AUTH0_CALLBACK_URL
- URL-адрес вашего приложения.AUTH0_API_AUDIENCE
Вы можете найти данные
AUTH0_CLIENT_ID
,AUTH0_DOMAIN
иAUTH0_API_AUDIENCE
на панели управления Auth0.
Нам нужно установить callback
, на который Auth0 перенаправляет. Перейдите в раздел Клиенты на панели управления. В настройках выставим обратный вызов http://localhost:3000
:
Имея учетные данные, давайте обновим наши компоненты React.
Компонент приложения
const AUTH0_CLIENT_ID = "aIAOt9fkMZKrNsSsFqbKj5KTI0ObTDPP"; const AUTH0_DOMAIN = "hakaselabs.auth0.com"; const AUTH0_CALLBACK_URL = location.href; const AUTH0_API_AUDIENCE = "golang-gin";
class App extends React.Component { parseHash() { this.auth0 = new auth0.WebAuth({ domain: AUTH0_DOMAIN, clientID: AUTH0_CLIENT_ID }); this.auth0.parseHash(window.location.hash, (err, authResult) => { if (err) { return console.log(err); } if ( authResult !== null && authResult.accessToken !== null && authResult.idToken !== null ) { localStorage.setItem("access_token", authResult.accessToken); localStorage.setItem("id_token", authResult.idToken); localStorage.setItem( "profile", JSON.stringify(authResult.idTokenPayload) ); window.location = window.location.href.substr( 0, window.location.href.indexOf("#") ); } }); }
setup() { $.ajaxSetup({ beforeSend: (r) => { if (localStorage.getItem("access_token")) { r.setRequestHeader( "Authorization", "Bearer " + localStorage.getItem("access_token") ); } } }); }
setState() { let idToken = localStorage.getItem("id_token"); if (idToken) { this.loggedIn = true; } else { this.loggedIn = false; } }
componentWillMount() { this.setup(); this.parseHash(); this.setState(); }
render() { if (this.loggedIn) { return <LoggedIn />; } return <Home />; } }
Мы обновили компонент приложения с помощью трех компонентных методов (setup
, parseHash
и setState
) и метода жизненного цикла componentWillMount
. Метод parseHash
инициализирует клиента auth0
webAuth
и анализирует хэш в более удобочитаемом формате, сохраняя их в localSt. Чтобы показать экран блокировки, захватите и сохраните токен пользователя и добавьте правильный заголовок авторизации к любым запросам к нашему API.
Домашний компонент
Наш компонент Home будет обновлен. Мы добавим функциональность для метода authenticate
, который будет запускать отображение размещенного экрана блокировки и позволять нашим пользователям входить в систему или регистрироваться.
class Home extends React.Component { constructor(props) { super(props); this.authenticate = this.authenticate.bind(this); } authenticate() { this.WebAuth = new auth0.WebAuth({ domain: AUTH0_DOMAIN, clientID: AUTH0_CLIENT_ID, scope: "openid profile", audience: AUTH0_API_AUDIENCE, responseType: "token id_token", redirectUri: AUTH0_CALLBACK_URL }); this.WebAuth.authorize(); }
render() { return ( <div className="container"> <div className="row"> <div className="col-xs-8 col-xs-offset-2 jumbotron text-center"> <h1>Jokeish</h1> <p>A load of Dad jokes XD</p> <p>Sign in to get access </p> <a onClick={this.authenticate} className="btn btn-primary btn-lg btn-login btn-block" > Sign In </a> </div> </div> </div> ); } }
Компонент LoggedIn
Мы обновим компонент LoggedIn
, чтобы он взаимодействовал с нашим API и снимал все шутки. Он будет передавать каждую шутку как prop
компоненту Joke
, который отображает панель начальной загрузки. Напишем те:
class LoggedIn extends React.Component { constructor(props) { super(props); this.state = { jokes: [] };
this.serverRequest = this.serverRequest.bind(this); this.logout = this.logout.bind(this); }
logout() { localStorage.removeItem("id_token"); localStorage.removeItem("access_token"); localStorage.removeItem("profile"); location.reload(); }
serverRequest() { $.get("http://localhost:3000/api/jokes", res => { this.setState({ jokes: res }); }); }
componentDidMount() { this.serverRequest(); }
render() { return ( <div className="container"> <br /> <span className="pull-right"> <a onClick={this.logout}>Log out</a> </span> <h2>Jokeish</h2> <p>Let's feed you with some funny Jokes!!!</p> <div className="row"> <div className="container"> {this.state.jokes.map(function(joke, i) { return <Joke key={i} joke={joke} />; })} </div> </div> </div> ); } }
Компонент шутки
Мы также обновим компонент Joke
, чтобы отформатировать каждый элемент шутки, переданный ему из родительского компонента (LoggedIn
). Мы также добавим метод like
, который будет увеличивать количество подобных шуток.
class Joke extends React.Component { constructor(props) { super(props); this.state = { liked: "", jokes: [] }; this.like = this.like.bind(this); this.serverRequest = this.serverRequest.bind(this); }
like() { let joke = this.props.joke; this.serverRequest(joke); } serverRequest(joke) { $.post( "http://localhost:3000/api/jokes/like/" + joke.id, { like: 1 }, res => { console.log("res... ", res); this.setState({ liked: "Liked!", jokes: res }); this.props.jokes = res; } ); }
render() { return ( <div className="col-xs-4"> <div className="panel panel-default"> <div className="panel-heading"> #{this.props.joke.id}{" "} <span className="pull-right">{this.state.liked}</span> </div> <div className="panel-body">{this.props.joke.joke}</div> <div className="panel-footer"> {this.props.joke.likes} Likes <a onClick={this.like} className="btn btn-default"> <span className="glyphicon glyphicon-thumbs-up" /> </a> </div> </div> </div> ) } }
Собираем все вместе
Завершив UI и API, мы можем протестировать наше приложение. Мы начнем с загрузки нашего сервера source .env && go run main.go
, а затем перейдем к http://localhost:3000
из любого браузера. Вы должны увидеть компонент Home
с кнопкой входа. Нажатие кнопки входа приведет к перенаправлению на размещенную страницу блокировки (создание учетной записи или входа в систему) для продолжения использования приложения.
Дом:
Размещенный экран блокировки Auth0.
Просмотр приложения "Вход в приложение"
Заключение
Поздравляю! Вы узнали, как создать приложение и API с помощью Go и фреймворка Gin.
Я пропустил что-то важное? Сообщите мне об этом в комментариях.
Вы можете поздороваться со мной в Твиттере @codehakase