20200702のGoに関する記事は5件です。

[LINE Bot] LIFFとリッチメニューでも管理画面が作りたい! 3 -認証編-

この記事はこの記事の続きです
[LINE Bot] LIFFとリッチメニューでも管理画面が作りたい! 2 -リッチメニュー切替-

ここまでのまとめ

さて、ここまでで「認証用のLINEグループを作る」「グループへの招待・退出」でメニューを切り替える、というところまでできました。
あとはLIFFで作った管理画面に飛ばせばOKですが、当然そのさきのAPIでも認証をかけないといけません。

構成図

ちなみに構成は次のようになっています。

  • LIFF : ユーザ用メニューと管理者メニュー (React)
  • bot : linebotのロジック(Go)
  • line_api : LIFFからLINEの認証などのLINE固有の処理をするAPI (Go)
  • omoinas : アプリケーションのメインロジックやmodel・repository層など (Rust)

image.png

LIFFでの認証の考え方

今回はログインしているユーザのLINE IDを見て管理者かどうかを判別します。

さて、LIFFでLINE IDを取得するにはsocial api 2.1 を使えば良さそうです。
が、公式マニュアルにもあるように、
LIFFでLINEのUserIDを取得して、それを直接サーバに送り付けてはいけません。
(直接、ユーザIDヲ指定してAPIを叩けてしまうとセキュリティ的によろしくない)

上記のドキュメントにある通り、LIFFではアクセストークンを取得し、サーバー側でプロフアイルを取得してユーザIDを確認します。

LIFFとWebページを作る

LIFFを使うには、LINE Developers から新しく「LINE Login」チャネルを作成します。
なお、現在はLine Messaging APIでLIFFアプリは作れません。

今回はReactで作りました。

Reac/TypescriptでLIFFを作るためのあれこれ

意外と面倒です。。。
今回は create-react-appで作りましたが、はじめGatsbyあたりを使おうとして色々読み込み方がわからず、というのがありました。。

LIFFを読み込む

public/index.html
<html>
  <head>
    <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
  </head>
  ...
</html>

Typescript用の定義ファイルを追加する。

npm install --save liff-type

環境変数にLIFFのIDを書く

.env
# 開発用
REACT_APP_DEV_LIFF_ID="xxxx"

# 本番環境用
REACT_APP_PRD_LIFF_ID="yyyy"

環境変数の切り替えについて

LIFFの開発では、UIの確認程度ならローカルでもできますが、
結局はサーバー上で確認しないとけないため、開発環境と本番環境の構築と切り替えをしないといけません。

process.env.NODE_ENVは、 npm startではdevelopmentになりますが、
npm run build すると強制的に prodcutionになる為、
今回は REACT_APP_ENVという変数を使い、npm run buildするときに環境変数を指定することにしました。

# 開発環境用の変数でビルドする
REACT_APP_ENV=dev npm run build

LIFF初期化

LIFF初期化の注意点としては、liff.login()をかける前はURLがエンコードされており、そのままではルーティングされません。
そのため、Routerの外側でまずLIFFの初期化を行い、LIFFがURLをデコードするときちんとルーティングされます。

src/App.tsx
import React from 'react';
import {
  BrowserRouter as Router,
  Route,
  Switch,
} from 'react-router-dom';


... 何やらかんやら初期化

function App() {
  liff.init({ liffId: (process.env.REACT_APP_ENV === "prd"
    ? process.env.REACT_APP_PRD_LIFF_ID
    : process.env.REACT_APP_DEV_LIFF_ID)as string }).then(() => {})
  return (
    <MuiThemeProvider theme={theme}>
      <CssBaseline />
      <Router>
        <Switch>
          <Route path="/" exact>
            This is liff.
          </Route>
          <Route path="/:env/user/omise/:clientId/:omiseId" exact>
            <ユーザ用のページ />
          </Route>

          <Route path="/:env/staff/omise/:clientId/:omiseId" exact>
            <管理用ペーシ />
          </Route>
        </Switch>
      </Router>
    </MuiThemeProvider >
  );
}

export default App;

管理者用のページでアクセストークンを取得する

公式ドキュメントに従い、LIFFの初期化とアクセストークンの取得をします。

src/StaffOmise.rs
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom';
import { useForm, Controller } from "react-hook-form";

// ↓サーバサイドのAPI
import {getOmise, Omise, setOmise, OmiseForm} from 'utils/api/omise';

... 色々コンポーネント読み込みとか

function StaffOmise() {
  console.log("aaaa")
  const {env, clientId, omiseId, charaId} = useParams<RouteParams>();

   ...

   // ロード時にデータ読み込む処理
  const load = () => {
    getOmise(env, clientId, omiseId, (omise: Omise) => {
       ... // フォームへの値の設定とか
    })
  }

    // 初期化処理
    useEffect(() => {
    liff.ready.then(() => {
      let accessToken = ""
      if (!liff.isLoggedIn()) {
         // ログインしていなければログインさせる。(ローカル環境ではスルーする)
        if (process.env.NODE_ENV === "production") {
          liff.login({})
        }
      } else {
        accessToken = liff.getAccessToken()
        setToken(accessToken)
      }
      load()
    })
  },[env, clientId, omiseId, charaId])

  return {
    ...フォームとか
  } 
}
export default StaffOmise;

API呼び出す時にLINEのトークンを付与する

上記でアクセストークンを取得したので、APIのヘッダーにつけます。

src/utils/api.tsx
// 更新API
export function setOmise(token: string, ...パラメータ) {
  fetch (
   env === 'prd'
      ? `${process.env.REACT_APP_PRD_LINE_API_HOST}/line-api/omise/set`
      : `${process.env.REACT_APP_DEV_LINE_API_HOST}/line-api/omise/set`,
    {
      method: "POST",
      mode: "cors",
      cache: "no-cache",
      headers: { 'Authorization': 'Bearer: '+token},
      body: JSON.stringify({
        ...更新するデータ
      }),
    }
  )... // thenとかcatchとか
}

サーバサイドで認証する

これでLIFFからLINEのアクセストークンが送信されますので、これを元にLINEのユーザIDを取得します。

line_api/set_omise.go
func setOmise(request events.APIGatewayProxyRequest) (string, error) {
    // ちょっと雑ですが、ヘッダーからアクセストークンを取得します。
    accessToken := strings.TrimPrefix(request.Headers["Authorization"], "Bearer: ")
    if accessToken == "" {
        // 401
        return "", errors.New("access token is empty.")
    }

    // ユーザ名取得
    prof, err := client.GetUserProfile(accessToken).Do()
    if err != nil {
        log.Println(err)
        return "", err
    }

    param = {...}
    param.UserID = prof.UserID

    ... // ラムダでRustの更新APIを呼び出し、そこでDynamoDBからユーザIDをチェックする。
    payload, _ := json.Marshal(param)
    res, err := lambda.New(session.New()).Invoke(&lambda.InvokeInput{
        FunctionName:   aws.String(ARN + "hoge-" + os.Getenv("ENV") + "-setOmise"),
        Payload:        payload,
        InvocationType: aws.String("RequestResponse"),
    })
    if err != nil {
        log.Println(err)
        return "", err
    }
}

(Rustで書いたラムダは当然、このAPIからしか呼び出せないように設定しておきます)

まとめ

以上で、LIFFで管理画面を作ってリッチメニューを切り替えたり認証したりする方法を紹介しました。
システムを開発すると、何かにつけて管理画面が必要になりますし、
作ったら作ったで、やれスマホ対応しろだのメアド・パスワード機能をつけろ、というのが世の常です。
ですが、管理画面なんてそもそもシンプルな方が良いですし、パスワード管理なんてしたくありません。
(Auth系サービスもありますが、結局、招待やらアカウント管理やら手間がかかります)

できるだけシンプルにしたい! という発想からの試みでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jenkinsのgolang crawler問題。

Jenkinsで昨日になって急に起き始めたMalformedURLException

java.net.MalformedURLException: no protocol: /dl/go1.14.2.linux-amd64.tar.gz
    at java.net.URL.<init>(URL.java:593)
    at java.net.URL.<init>(URL.java:490)
    at java.net.URL.<init>(URL.java:439)
    at org.jenkinsci.plugins.golang.GolangInstaller.performInstallation(GolangInstaller.java:57)
    at hudson.tools.InstallerTranslator.getToolHome(InstallerTranslator.java:72)
    at hudson.tools.ToolLocationNodeProperty.getToolHome(ToolLocationNodeProperty.java:109)
    at hudson.tools.ToolInstallation.translateFor(ToolInstallation.java:206)
    at org.jenkinsci.plugins.golang.GolangInstallation.forNode(GolangInstallation.java:44)
    at org.jenkinsci.plugins.golang.GolangInstallation.forNode(GolangInstallation.java:22)
    at org.jenkinsci.plugins.workflow.steps.ToolStep$Execution.run(ToolStep.java:152)
    at org.jenkinsci.plugins.workflow.steps.ToolStep$Execution.run(ToolStep.java:133)
    at org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution$1$1.call(SynchronousNonBlockingStepExecution.java:49)
    at hudson.security.ACL.impersonate(ACL.java:260)
    at org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution$1.run(SynchronousNonBlockingStepExecution.java:46)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:748)
Finished: FAILURE

なんだこりゃと思って、色々試してもあんまり変わらない。
今日になってネットで調べてみると以下の記事が見つかった。
https://qiita.com/cpp0302/items/27c1747bc3f9d753eb64

これそのものズバリだったので、対処も同様に行った。
これ大本は↓に書いてあるように、golangの仕様変更に伴って、jenkinsのcrawlerが対応できていなかったことなんだよね。

https://github.com/jenkins-infra/crawler/pull/89
https://github.com/golang/website/commit/58a31798e86f7e67791de8767e8353f734576199#diff-8dfde61e35bbbe9e94ea52fe96273aa9L280-R280

このcrawlerが変わると、一体どういう感じで反映されるんだろう…。自動ダウンロードがどういう仕組みになっているのかいまいちわからん。

In Manage Jenkins > Global Tool Configuration, you can edit your Go installation(s) to replace the "Install from golang.org" installer with the "Install from .tar.gz" installer, then provide the appropriate download URL, and the subdirectory (probably "go")

https://issues.jenkins-ci.org/browse/JENKINS-62887

ワークアラウンドとして上記を採用したけど日本語版だと、Top→左ペインのJenkinsの管理Global Tool Configurationgoの欄ですね。
いやーこの辺最近触ってなかったので完全に見失った(笑)

また、設定する場合、DLしたディレクトリの直下ですね。(新規構築でもない限り普通はあるはず。)
つまり、
${JENKINS_HOME}/tools/org.jenkinsci.plugins.golang.GolangInstallation/(Jenkinsに設定されている名前)
これで良いはず。

補足

私は、${JENKINS_HOME}/tools/org.jenkinsci.plugins.golang.GolangInstallation/1.14配下にインストール済みだったんだけど、

[jenkins]$ ls -ltr
合計 204
-rw-r--r--  1 jenkins jenkins   397  4月  9 04:15 SECURITY.md
-rw-r--r--  1 jenkins jenkins  1607  4月  9 04:15 README.md
-rw-r--r--  1 jenkins jenkins  1303  4月  9 04:15 PATENTS
-rw-r--r--  1 jenkins jenkins  1479  4月  9 04:15 LICENSE
-rw-r--r--  1 jenkins jenkins 90098  4月  9 04:15 CONTRIBUTORS
-rw-r--r--  1 jenkins jenkins  1339  4月  9 04:15 CONTRIBUTING.md
-rw-r--r--  1 jenkins jenkins 55383  4月  9 04:15 AUTHORS
-rw-r--r--  1 jenkins jenkins    26  4月  9 04:15 robots.txt
-rw-r--r--  1 jenkins jenkins  5686  4月  9 04:15 favicon.ico
-rw-r--r--  1 jenkins jenkins     8  4月  9 04:15 VERSION
drwxrwxr-x  2 jenkins jenkins   312  4月 30 18:05 api
drwxrwxr-x  2 jenkins jenkins    29  4月 30 18:05 bin
drwxrwxr-x  3 jenkins jenkins    18  4月 30 18:05 lib
drwxrwxr-x  7 jenkins jenkins  4096  4月 30 18:05 doc
drwxrwxr-x 12 jenkins jenkins   165  4月 30 18:05 misc
drwxrwxr-x  6 jenkins jenkins    76  4月 30 18:05 pkg
drwxrwxr-x 47 jenkins jenkins  4096  4月 30 18:05 src
drwxrwxr-x 23 jenkins jenkins 12288  4月 30 18:05 test

goのバイナリがあるのは、bin配下だから、最初
${JENKINS_HOME}/tools/org.jenkinsci.plugins.golang.GolangInstallation/1.14/binと設定したら、以下のようにエラーが出た。

[xxxx-THAL7GIC7EP2KG363RGLDNNRHS6T5VIFM7P5FDFXNXB5U4DDPX3Q] Running shell script
+ go env -w CGO_ENABLED=0
/jenkins_home/workspace/xxx-THAL7GIC7EP2KG363RGLDNNRHS6T5VIFM7P5FDFXNXB5U4DDPX3Q@tmp/durable-27fdf3a0/script.sh: 行 2: go: コマンドが見つかりません

補足2

起きるbranchと起きないbranchがある。
crawlする条件があるんだろうけど、そこまで調査していない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでGoを試す環境を作った話

はじめに

A Tour of Goをやろうと思ったんだけど、手元で試したいということがあったので、Dockerで開発環境を構築

完成図

.
├── Dockerfile
├── docker-compose.yml
└── src
    └── hello.go

作成

準備するものは以下の通り。

.
├── Dockerfile
└── docker-compose.yml

ファイルの中身

Dockerfile
FROM golang:latest
WORKDIR /go/src/
docker-compose.yml
version: "3"
services: 
  go:
    build: .
    tty: true
    volumes:
      - ./src:/go/src

docker-composeを利用した理由は、マウントしたかったから。

うごかす

コマンドは以下の通り。

# コンテナの作成と起動
$ docker-compose up -d --build

# コンテナ内に入る
$ docker-compose exec go bash

Hello world

コンテナの中に入っている状態でファイル作成

$ touch hello.go
hello.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World")
}

コンパイルは以下のようになる。

$ go run hello.go

とめる

# コンテナから出る
$ exit

# コンテナを止める
$ docker-compose stop

# コンテナを削除
$ docker-compose down

おわりに

A Tour of Goを試していって、つまったところがあれば修正していきます。

参考

DockerでGoの開発環境を構築する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今からサービスを考えるなら、SE向けが堅い。少なくとも、男性向けにしよう。

最近、暇で以下のサービスを作った。
https://deau-project.herokuapp.com/

このサービスを、Heroku, React, Go Gin で実装した。
Heroku なので無料だ。
請求先の登録も不要である。

React 以下の、material-ui のテンプレートを使用した。
https://github.com/mui-org/material-ui/tree/master/docs/src/pages/getting-started/templates/album
https://github.com/mui-org/material-ui/tree/master/docs/src/pages/getting-started/templates/sign-in
https://react-hook-form.com/jp/

サービスを開始してから、Qiita でブログを始めた。
ブログを始めた次の日のPV, UUが、500弱だった。
9割は、Qiita からである。

以前の記事で書いたが、無料でサービスを開始できる。
ただ、利用者を増やすにはどうしようか考えていた。

無料で広告するなら、Qiita で記事を書くといい。
ちなみに、note でも同じく記事を書いているが、
note からは数人しか来てなかった。

Qiita からのアクセスは、広告に匹敵する。
もし、サービスを考える時に、この事実を知っていたら、SE向け、男性向けにしていただろう。
残念。

作ったサービスはメール送信するサービス。
SEの性かもしれないが、送信までしてくれる人が多い。

もし、あなたがサービスを考えている途中なら、是非、考慮してもらいたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでサーバーとクライアントを実装してみた。

こんにちは。そうはぶです。
「GoでAPIを使ってみたら心が3回くらい折れた話。」で書かせていただいたように一度API利用で挫折しました。再挑戦するときにHTTPの基礎を学んで、Goでサーバーとクライアントを実装してリクエストとレスポンスを体感してからだとAPIの理解が非常にスムーズになったので、Goによるサーバーとクライアントの実装をご紹介させてください。

エコーサーバーの実装

とりあえずエコーサーバーを作ります。
これは通信内容をそのままコンソールに表示してくれます。
僕もでしたが、内容がよくわからなくてもとりあえずコピペしていただければ大丈夫です。

server.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "net/http/httputil"
)

func handler(w http.ResponseWriter, r *http.Request) {
    dump, err := httputil.DumpRequest(r, true)
    if err != nil {
        http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
        return
    }
    fmt.Println(string(dump))
    fmt.Fprintf(w, "<html><body>Hello World</body></html>")
}

func main() {

    var httpServer http.Server
    http.HandleFunc("/", handler)
    log.Println("start http listing :8080")
    httpServer.Addr = ":8080"
    log.Println(httpServer.ListenAndServe())

}

エコーサーバーが完成したらエコーサーバーは先に実行して走らせおきます。

クライアントの実装

次はクライアントを実装していきます。

GETメソッドの送信

まずは一番シンプルなGETメソッドを送信してみます。

client.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

//baseURLを定義
var baseURL = "http://localhost:8080/"

func main() {
    //baseURL(http://localhost:8080/)にGETでアクセス
    resp, err := http.Get(baseURL)
    if err != nil {
        log.Fatal(err)
    }
    //お決まり
    defer resp.Body.Close()

    //bodyにrespのボディ部分を入れる
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    //[]byte型で返ってくるのでstring型にキャストして出力
    fmt.Println(string(body))

}

これを実行するとコンソールはそれぞれ以下のようになります。

server.goのコンソール画面
GET / HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip
User-Agent: Go-http-client/1.1
client.goのコンソール画面
<html><body>Hello World</body></html>

server.goのコンソール画面にはリクエストが出力されて、client.goには受け取ったレスポンスのボディ部が出力されました。

GETメソッド+クエリーの送信

client.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
)

//baseURLを定義
var baseURL = "http://localhost:8080/"

func main() {

    //クエリー文字列を作る
    values := url.Values{
        "q1": {"souhub"},
        "q2": {"github"},
    }
    //valuesを文字列に変換
    query := values.Encode()
    //文字列を結合させてurlを定義
    url := baseURL + "?" + query

    //url(http://localhost:8080/)にGETでアクセス
    resp, err := http.Get(url)
    if err != nil {
        log.Fatal(err)
    }
    //お決まり
    defer resp.Body.Close()

    //bodyにrespのボディ部分を入れる
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    //[]byte型で返ってくるのでstring型にキャストして出力
    fmt.Println(string(body))

}

urlの定義以降は先程と同様です。実行するとコンソール画面はそれぞれ以下のようになります。

server.go
GET /?q1=souhub&q2=github HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip
User-Agent: Go-http-client/1.1
client.go
<html><body>Hello World</body></html>

クライアントのコンソール画面は変化ありませんが、サーバー側はパスが変わりました。
URIでq1にsouhubを、q2にgithubという情報を渡していることがわかります。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む