Эта статья изначально была размещена в Моем блоге

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 &nbsp;
            <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 &nbsp;
            <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