- 投稿日:2020-07-02T21:29:02+09:00
[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)
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 buildLIFF初期化
LIFF初期化の注意点としては、liff.login()をかける前はURLがエンコードされており、そのままではルーティングされません。
そのため、Routerの外側でまずLIFFの初期化を行い、LIFFがURLをデコードするときちんとルーティングされます。src/App.tsximport 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.rsimport 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.gofunc 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系サービスもありますが、結局、招待やらアカウント管理やら手間がかかります)できるだけシンプルにしたい! という発想からの試みでした。
- 投稿日:2020-07-02T15:48:14+09:00
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 Configuration
のgo
の欄ですね。
いやーこの辺最近触ってなかったので完全に見失った(笑)また、設定する場合、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 testgoのバイナリがあるのは、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する条件があるんだろうけど、そこまで調査していない。
- 投稿日:2020-07-02T13:49:32+09:00
DockerでGoを試す環境を作った話
はじめに
A Tour of Goをやろうと思ったんだけど、手元で試したいということがあったので、Dockerで開発環境を構築
完成図
. ├── Dockerfile ├── docker-compose.yml └── src └── hello.go作成
準備するものは以下の通り。
. ├── Dockerfile └── docker-compose.ymlファイルの中身
DockerfileFROM golang:latest WORKDIR /go/src/docker-compose.ymlversion: "3" services: go: build: . tty: true volumes: - ./src:/go/srcdocker-composeを利用した理由は、マウントしたかったから。
うごかす
コマンドは以下の通り。
# コンテナの作成と起動 $ docker-compose up -d --build # コンテナ内に入る $ docker-compose exec go bashHello world
コンテナの中に入っている状態でファイル作成
$ touch hello.gohello.gopackage main import "fmt" func main() { fmt.Println("Hello, World") }コンパイルは以下のようになる。
$ go run hello.goとめる
# コンテナから出る $ exit # コンテナを止める $ docker-compose stop # コンテナを削除 $ docker-compose downおわりに
A Tour of Goを試していって、つまったところがあれば修正していきます。
参考
- 投稿日:2020-07-02T10:01:15+09:00
今からサービスを考えるなら、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の性かもしれないが、送信までしてくれる人が多い。もし、あなたがサービスを考えている途中なら、是非、考慮してもらいたい。
- 投稿日:2020-07-02T09:19:19+09:00
Goでサーバーとクライアントを実装してみた。
こんにちは。そうはぶです。
「GoでAPIを使ってみたら心が3回くらい折れた話。」で書かせていただいたように一度API利用で挫折しました。再挑戦するときにHTTPの基礎を学んで、Goでサーバーとクライアントを実装してリクエストとレスポンスを体感してからだとAPIの理解が非常にスムーズになったので、Goによるサーバーとクライアントの実装をご紹介させてください。エコーサーバーの実装
とりあえずエコーサーバーを作ります。
これは通信内容をそのままコンソールに表示してくれます。
僕もでしたが、内容がよくわからなくてもとりあえずコピペしていただければ大丈夫です。server.gopackage 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.gopackage 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.gopackage 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.goGET /?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という情報を渡していることがわかります。参考