LoginSignup
60
29

More than 3 years have passed since last update.

はじめに

APIを機能別にDockerコンテナー化(マイクロサービス、と言うらしい)したのはいいけれど、認可(アクセスコントロール)はどうしよう、という話です。

Istioは、Kubernetesの最小管理単位Podが、Dockerコンテナーを1つ以上配置できることを利用して、DockerコンテナーにIstioのプロキシー(Envoy)をinjectして(リバースだけでは無いみたいなので、単に「プロキシー」と書いておきます)、各Dockerコンテナーにプロキシーを入れてしまおう、という考え方です。

2018-12-11_105350.png

実は、昔Istio 0.7時代にも試したことがあるのですが、そのときはエンドユーザーの権限(≒エンドユーザーのアクセストークン)による認可ができなかったのですが(実装はあったらしいのですが、ドキュメントに記載が全く無かったため、何をどう設定すればよいか分からなかった)、Istio 0.8というか1.0で実装された、といううわさを聞いたので、改めてやることにしました。そのため、今回の記事ではIstio自体の説明や細かいインストール手順は省略させていただきます。そこまで書くと、とてもQiita1ページに収まる内容ではないので。

本記事の対象ではない読者

  • APIの認可をやりたいが、どうすればよいか困っている人
    • 今回は解決しないんだ…
  • C#(ASP.NET core)で、可読性が高くメンテナンス性に優れた、クールな認可処理の実装方法を知りたい人
    • 今回はプログラムの話じゃないんだ…
  • Istioに興味は無いが、EKSの使い方を知りたい人
    • 今回はEKS使ってないんだ…

ソフトウェアのバージョン

OS は CentOS 7.5です。

ソフトウェア名 バージョン
Docker 1.13 (CentOS 7.5でyumしたら入った)
Kubernetes 1.12.2 (2018年11月時点の最新)
Istio 1.0.3 (2018年11月時点の最新)
Keycloak 4.1 (2018年11月時点の最新ではないが、特に意味は無い)

Istioデモ - Bookinfoの簡単な説明

自作のAPIでやってみたかったのですが、あまり時間がなかったため、Istioについているデモ Bookinfo を使うことにしました。このアプリケーションを簡単に説明しておきます。
※破線四角はPod。実線四角で表している各APIはDockerコンテナー。

2018-12-06_115949.png

  • エンドユーザーは productpage にアクセスする。逆に言うと、エンドユーザーが直接アクセスするAPIは、productpageだけである。
  • 最上部の領域にある「Sign in」ボタンは使わない。
  • productpage は、左下の領域に表示する本の詳細情報であるdetailsと、右下の領域に表示するレビューであるreviewsがあり、productpageからdetailsreviews(3つ)のAPIを呼び出している。
  • reviewsにはv1, v2, v3の3つがあり、productpageからはどれか1つランダムに呼び出す。つまり、F5連打すると、reviewsが切り替わる。(下の画像はv3が呼び出されている)
  • reviews(v2とv3)はレーティング情報としてratingsのAPIを呼び出している。そのレーティング値を、v2は黒星で、v3は赤星で表示している。

2018-12-05_111644.png

今日やろうとすること - 権限の制御

認可では一番単純な例となってしまいますが、アクセストークンのclaimに "email":"u001@hoge.example.co.jp" があるユーザーのみAPIを許可するようにします。本当はグループやロールによる制御や、scopeの値を使っての制御にしたかったのですが、そこまでできませんでした。

2018-12-06_121105.png

Kubernetesのインストール(メモだけ)

AWS EKSを使いたかったのですが、この記事を書いた時点(2018年11月)ではまだ東京リージョンにサービスがなかったので、仕方なくEC2上に構築しました :cry:

EC2インスタンス上での構築は、昔私が書いた記事(前編後編)を参照してください。1.10 → 1.12 の変更点だけ列挙しておきます。

  • masterに、/etc/systemd/system/kubelet.service.d/10-kubeadm.confEnvironment="KUBELET_CGROUP_ARGS=--cgroup-driver=systemd"を入れる必要が無くなった、というかこの設定自体が無くなった。
  • flannelインストール時の、yamlファイルの場所が変わった。kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.ymlkubectl apply -f https://raw.githubusercontent.com/coreos/flannel/bc79dd1505b0c8681ece4de4c0d86c5cd2643275/Documentation/kube-flannel.yml

Istioのインストール(メモだけ)

基本的には、Istioのドキュメントを見てください(英語ですが)。しかし、手順どおりやってもうまくいかないので、注意点を列挙しておきます。

Istioのダウンロードとインストール

ダウンロードは curl を使っているので、プロキシー環境下では、curl にプロキシー設定がいる。~/.curlrc というファイルを作って次のように書く。

proxy-user = "(ユーザー名):(パスワード)"
proxy = "http://(プロキシーサーバーのFQDN):(プロキシーサーバーのポート番号)"

Istioのセットアップ

  • Option 1~4 まであるが、helmじゃないとセットアップに成功しない。つまり Option 1と2は使えない。Option 1か2でセットアップすると、Envoyがリクエストを受け付けてくれず、路頭に迷う。(エラーメッセージ紛失。多分 upstream connect error or disconnect/reset before headers
  • helm template では、ドキュメントに書いていないが、--set global.configValidation=false オプションを付けるのが必須 。これを付けないと istio-galley で failed calling admission webhook "pilot.validation.istio.io": Post https://istio-galley.istio-system.svc:443/admitpilot?timeout=30s: Not Found が出続けて路頭に迷う。
  • Istioセットアップ後は、istio-ingressgatewayサービスの spec.typeNodePort に変更する。EC2インスタンスでは、外部IP(EXTERNAL-IP)が付けられないため。

サンプル Bookinfo のセットアップ

  • samples/bookinfo/networking/bookinfo-gateway.yaml の中にある GatewayVirtualService は次のものを使う(※VirutalServicehostの値が違う)
$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: productpage-gateway
spec:
  selector:
    istio: ingressgateway # use Istio default gateway implementation
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
EOF

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: productpage
spec:
  hosts:
  - "*"
  gateways:
  - productpage-gateway
  http:
  - match:
    - uri:
        exact: /productpage
    - uri:
        exact: /login
    - uri:
        exact: /logout
    - uri:
        prefix: /api/v1/products
    route:
    - destination:
        port:
          number: 9080
        host: productpage.default.svc.cluster.local
EOF
  • BookinfoへのアクセスURLは、istio-ingressgateway経由になるので、http://(KubernetesのmasterのFQDN):(isto-ingressgatewayのポート)/productpage になる。isto-ingressgateway のポート番号(NodePort)は、次のコマンドを実行して、80ポートに紐づいている値になる(次の例では31380)。
$ kubectl get service istio-ingressgateway -n istio-system
NAME                   TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                                                                                   AGE
istio-ingressgateway   NodePort   10.103.184.198   <none>        80:31380/TCP,443:31390/TCP,31400:31400/TCP,15011:30531/TCP,8060:32322/TCP,853:32651/TCP,15030:32213/TCP,15031:32726/TCP   6d

OpenID Providerの設定 - Keycloakの設定

実際にAPIの認可をやる前に、JWT(アクセストークン)を発行するOPの設定をちゃちゃっとやってしまいます。

Clientの設定

要点だけ、箇条書きにしておきます。

  • Client IDは何でもよいが、この記事ではistioにした。(クライアントシークレットはKeycloakが自動生成する)
  • 同意画面は出したくないので、Consent RequiredはOFF。
  • Client Protocolは当然openid-connect
  • Access Typeは、普通のWebサーバーなので(クライアントシークレットを秘密にできるので)、confidentialに変えておく。
  • Flowは、面倒なので、全部使えるようONにしておく。
  • この記事では、アクセストークンの発行は(面倒なので)リソースオーナーパスワードクレデンシャルを使うので、Valid Redirect URIsは適当な値に。本当は、APIの認証を行うWebアプリケーションのURLにする必要がある。
  • それ以外の項目は、デフォルト。

2018-12-05_111948.png

Client Scopeの設定

スコープにopenid以外である、address, phone, profile, emailを、認可リクエストでスコープに指定した場合返すように設定しました。しかしこの記事のケースでは、別にこのような設定にする必要はありませんでした。

2018-12-05_112645.png

ユーザーを作る

権限がある u001 と権限がない u002 をつくりました。

2018-12-05_112851.png

アクセストークンの検証 - 認証ポリシー (Authentication Policy)

ここから、認可を行っていきます。まず、認可に使うアクセストークン自体の検証をする設定を行います。

:information_source: Istioのドキュメントでは、「アクセストークンを使う」とは一言も書かれていません。ただ「JWTの検証をする」とだけ書かれているだけであり、OpenID Connectの何トークンを使うべきか、は何も書かれていません。さらにこの機能は、認可ではなく「認証」と書かれているので、JWTを要求することを合わせると、IDトークンが正しいのかもしれません。しかし通常はIDトークンの有効期限は短く(1分~5分程度)、APIにアクセスする度にトークンを発行するために認可リクエストを送るのはおかしいので、私は(リフレッシュトークンができる)アクセストークンであると解釈しました。さらに言うと、この後でやる認可(Authorization)では、`Authorization: Bearer`ヘッダーにあるJWTを参照し、認証ポリシー(Authentication Policy)でも`Authorization: Bearer`ヘッダーを参照するので、アクセストークンでないと都合が悪い、という理由もあります。

なぜかドキュメントにはBookinfo用の認証ポリシー(Authentication Policy)が無いので、自分で作ります。

ポリシー(Policy)の定義

アクセストークンの検証として、issuerの値と署名検証用のjwks_uriを設定します。設定は見たまま、特に難しいところは無いと思います。ちなみにoriginsというのが、Authorization: Bearerヘッダーの値を意味しているらしいです。昔(Istio 0.7)は、リクエストのどの値を使うか、RequestContext に定義したのですが、どこかにいってしまったようです。

$ cat <<EOF | kubectl apply -f -
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "productpage-jwt-example"
spec:
  targets:
  - name: productpage
  peers:
  - mtls: {}
  origins:
  - jwt:
      issuer: "http://172.26.22.76/auth/realms/sample"
      jwksUri: "http://172.26.22.76/auth/realms/sample/protocol/openid-connect/certs"
  principalBinding: USE_ORIGIN
EOF

DestinationRule

ドキュメントにはmutual TLSでないとダメ、と書かれているので、そういう設定をしておきます。

productpage の DestinationRule では、spec.hostの値は、productpageでもなくproductpage.default.svcでもなく、productpage.default.svc.cluster.localみたいです。

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: productpage
spec:
  host: "productpage.default.svc.cluster.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
  subsets:
  - name: v1
    labels:
      version: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: ratings
spec:
  host: ratings
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v2-mysql
    labels:
      version: v2-mysql
  - name: v2-mysql-vm
    labels:
      version: v2-mysql-vm
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: details
spec:
  host: details
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---
EOF

試してみる

まずは、アクセストークンなしでリクエストを送ってみます。401(403でも500でも503でもなく)が出ればOKです。
※172.26.22.81はKubernetes masterのIPアドレス、31380はistio-ingressgatewayのNodePort

$ curl -s -D - -X GET http://172.26.22.81:31380/productpage
HTTP/1.1 401 Unauthorized
content-length: 29
content-type: text/plain
date: Tue, 04 Dec 2018 08:00:48 GMT
server: envoy
x-envoy-upstream-service-time: 0

Origin authentication failed.

次に、Keycloakからアクセストークンを発行して、それをAuthorization: Bearerヘッダーに付けて、リクエストを送ってみます(Keycloakからのレスポンスは見やすいように改行していますが、実際は1行です)。
※172.26.22.76はKeycloakのIPアドレス

$ curl -s -X POST -H "Content-Type: application/x-www-form-urlencoded" -H "Authorization: Basic aXN0aW86OTExZjU2ZTAtMmU2ZS00NGYzLTlhMjgtOWNlYzg2MDY4ZjIy" -d "username=u001&password=password&grant_type=password&scope=openid%20email" 
 http://172.26.22.76/auth/realms/sample/protocol/openid-connect/token
{
  "access_token":"eyJhbGciOiJSUzI...(snip)...",
  "expires_in":3600,
  "refresh_expires_in":7199,
  "refresh_token":"eyJhbGciOiJSUzI...(snip)...",
  "token_type":"bearer",
  "id_token":"eyJhbGciOiJSUzI...(snip)...",
  "not-before-policy":0,
  "session_state":"f074aae8-bc50-4da9-9c52-b3f06c1f2693",
  "scope":"openid email"
}

$ curl -s -D - -X GET -H "Authorization: Bearer (アクセストークン)" http://172.26.22.81:31380/productpage
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
content-length: 4415
server: envoy
date: Wed, 05 Dec 2018 03:43:33 GMT
x-envoy-upstream-service-time: 991

<!DOCTYPE html>
<html>
  <head>
    <title>Simple Bookstore App</title>

...(snip)...

アクセストークンによる制御 - 認可 (Authorization)

ようやくAPIレベルの認可ですが、これもなぜかドキュメントにエンドユーザーの権限による認可の記述がないため、自分で作ります。

RBACを有効にする

$ kubectl apply -f <(istioctl kube-inject -f samples/bookinfo/platform/kube/bookinfo-add-serviceaccount.yaml) 
$ cat <<EOF | kubectl apply -f -
apiVersion: "rbac.istio.io/v1alpha1"
kind: RbacConfig
metadata:
  name: default
spec:
  mode: 'ON_WITH_INCLUSION'
  inclusion:
    namespaces: ["default"]
EOF
:warning: ドキュメントにも書かれていますが、設定の反映(浸透?)には時間が掛かります。早ければ1分程度、遅いと5分くらい掛かるようです。

試してみる1

全APIが不許可なので、403(401でも500でも503でもなく)が出ます。

$ curl -s -D - -X GET -H "Authorization: Bearer <アクセストークン>" http://172.26.22.81:31380/productpage
HTTP/1.1 403 Forbidden
content-length: 19
content-type: text/plain
date: Wed, 05 Dec 2018 03:48:26 GMT
server: envoy
x-envoy-upstream-service-time: 9

RBAC: access denied

productpageを認可 - ServiceRoleとServiceRoleBinding

アクセストークンに "email": "u001@hoge.example.co.jp" を持つユーザー(=u001)だけ許可するよう、serviceroleとservicerolebindingを設定します。

$ cat <<EOF | kubectl apply -f -
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
  name: productpage-viewer
spec:
  rules:
  - services: ["productpage.default.svc.cluster.local"]
    methods: ["GET"]
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRoleBinding
metadata:
  name: bind-productpage-viewer
spec:
  subjects:
  - properties:
      request.auth.claims[email]: "u001@hoge.example.co.jp"
  roleRef:
    kind: ServiceRole
    name: "productpage-viewer"
EOF

ServiceRoleServiceRoleBindingの内容の意味は、ドキュメントにコンセプトが書いてあります。

  • ServiceRole defines a group of permissions to access services.
  • ServiceRoleBinding grants a ServiceRole to particular subjects, such as a user, a group, or a service.

The combination of ServiceRole and ServiceRoleBinding specifies: who is allowed to do what under which conditions. Specifically:

  • who refers to the subjects section in ServiceRoleBinding.
  • what refers to the permissions section in ServiceRole.
  • which conditions refers to the conditions section you can specify with the Istio attributes in either ServiceRole or ServiceRoleBinding.
  • ServiceRoleは、サービスへアクセスするパーミッションのグループを定義します。
  • ServiceRoleBindingは、ユーザー、グループ、サービスといった、個々の主体(subjects)へServiceRoleを承認します。

ServiceRoleServiceRoleBindingによって、どのような条件下で何を許可するか、が設定されます。
具体的には、

  • 「誰が」は、ServiceRoleBindingのsubjectセクションで表します。
  • 「何を」は、ServiceRoleのパーミッションセクションで表します。
  • 「どのような条件」は、ServiceRoleServiceRoleBindingのどちからにある、Istio属性を使って設定できる、条件セクションで表します。

試してみる2

curlなので超絶見にくいですが、ユーザーu001の場合、productpageは表示できるが、detailsとreviewsが表示できていないことが確認できます。

$ curl -s -D - -X GET -H "Authorization: Bearer <u001ユーザーのアクセストークン>" http://172.26.22.81:31380/productpage
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
content-length: 3955
server: envoy
date: Wed, 05 Dec 2018 03:52:18 GMT
x-envoy-upstream-service-time: 36

<!DOCTYPE html>
<html>
  <head>
    <title>Simple Bookstore App</title>

...(snip)...

  <div class="row">
    <div class="col-md-6">
        <h4 class="text-center text-primary">Error fetching product details!</h4>
          <p>Sorry, product details are currently unavailable for this book.</p>
    </div>

    <div class="col-md-6">
        <h4 class="text-center text-primary">Error fetching product reviews!</h4>
          <p>Sorry, product reviews are currently unavailable for this book.</p>
    </div>

...(snip)...

$ curl -s -D - -X GET -H "Authorization: Bearer <u002ユーザーのアクセストークン>" http://172.26.22.81:31380/productpage
HTTP/1.1 403 Forbidden
content-length: 19
content-type: text/plain
date: Wed, 05 Dec 2018 03:56:07 GMT
server: envoy
x-envoy-upstream-service-time: 4

RBAC: access denied

detailsとreviewsを許可

productpageと同じように、detailsとreviewsのServieRoleとServiceRoleBindingを設定します。

$ cat <<EOF | kubectl apply -f -
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
  name: details-reviews-viewer
spec:
  rules:
  - services: ["details.default.svc.cluster.local", "reviews.default.svc.cluster.local"]
    methods: ["GET"]
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRoleBinding
metadata:
  name: bind-details-reviews
spec:
  subjects:
#  - user: "cluster.local/ns/default/sa/productpage"
  - properties:
      request.auth.claims[email]: "u001@hoge.example.co.jp"
  roleRef:
    kind: ServiceRole
    name: "details-reviews-viewer"
EOF

試してみる3

同じようにユーザーu001のアクセストークンを使ってアクセスします。detailsとreviewsの部分が出力されるようになっているはずです。

$ curl -s -D - -X GET -H "Authorization: Bearer <u001ユーザーのアクセストークン>" "http://172.26.22.81:31380/productpage"
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
content-length: 3955
server: envoy
date: Thu, 06 Dec 2018 01:24:30 GMT
x-envoy-upstream-service-time: 39

<!DOCTYPE html>
<html>
  <head>
    <title>Simple Bookstore App</title>

...(snip)...

  <div class="row">
    <div class="col-md-6">
        <h4 class="text-center text-primary">Error fetching product details!</h4>
          <p>Sorry, product details are currently unavailable for this book.</p>
    </div>

    <div class="col-md-6">
        <h4 class="text-center text-primary">Error fetching product reviews!</h4>
          <p>Sorry, product reviews are currently unavailable for this book.</p>
    </div>

...(snip)...

…うーん、ダメみたいですね。

認可(Authorization)の設定として、ServiceRoleBindspec.subjects

spec:
  subjects:
  - properties:
      request.auth.claims[email]: "u001@hoge.example.co.jp"

と書きました。このrequestは、見たとおりHTTPリクエストを表しているようなのですが、これはAPIが(というかAPIの前にいるEnvoyが)直接受け取るリクエストのことのようです。つまり、detailsとreviewsはproductpage内のAPIからリクエストを投げているので、productpage内のアプリケーションが気を利かせてリクエストヘッダーを設定してあげないと、この設定が効かないようです。私の予想では、istio-ingressgatewayからリクエストを通せば、後はIstioが勝手に各EnvoyにAuthorizationヘッダーにアクセストークンを付けてくれると思っていたのですが、残念ながらそうなっていないようです。(もし「そんなはずはない」「こうすればできる」という情報をお持ちのかたがいらっしゃれば、コメントに書いてくれると嬉しいです)

まとめ、というより感想

残念な結果で終わってしまいましたが、0.7時代と比較すると、アクセストークン(というか厳密にはJWT)の検証をして、Authorization: BearerにあるJWTを使ってくれるようになりました。(0.7時代は X-hogehoge というカスタムヘッダーでユーザー属性を直接リクエストに付ける必要があった --- 実際は0.7でもすでに実装はあったらしいですが)

Istio自体について言うと、まだ使いこなすにはものすごく厳しいプロダクトだと感じました。設定方法自体は整然としていて、すべてがうまくいっているときはちゃんと動くのですが、何か間違ったことをしてエラーや期待する結果にならなかったとき、調べる手段が無い(かよく分からない)というのが本当に厳しいです。調査方法の定石は、ログを見たりtcpdumpを見たりするのですが、Istioの場合ログがどこどう出るのか全く分かりません(ドキュメントの奥底深く探せばあるのかもしれませんが)。tcpdumpも、Envoy<->Dockerコンテナー内のアプリ間の通信を見なければならないのですが、Envoy上でtcpdumpの取り方が良く分かりません(ドキュメントに書いてあることを 0.7 時代にやりましたが、全く取得できませんでした)。

仮に、Envoy上でリクエストやレスポンスを見ることができたとしても、Envoyがどういうリクエストを期待しているかが分からないため、何が正しいかが分かりません。この辺がドキュメントに記載されるようにならないと、エラーが出たとき、コミュニティに質問を投げる以外、何もできません。Istioを試したことがある人の中には、インストール直後にEnvoyがリクエストを阻んで 503 が出続け、断念した人もいるのではないでしょうか。こういった状況になったとき、調べる手段が無いのは、まだ実用レベルには遠いと感じます。

認可(Authorization)の設定についても、ServiceRoleBindingspec.subjectsWhoを書く、とありましたが、userに書く内容がドキュメントからは良く分かりません。user: "service-account-a"サービスアカウントを表すと書かれていますが、サービスアカウントを表す文字列がどういうルールなのかの記載がありません。今回の例では、productpageのサービスカウント名が何なのか、最後まで分かりませんでした。サンプルではcluster.local/ns/default/sa/productpageとなっていますが、自分の試した環境ではこの値を設定しても403になるので、どうも違う値のようです(そしてこの値がどこから出てきたのかも分からない)。

そういう訳で、Istio全体としては、あとMajor番号が2つくらい上がらないと、人間が使うには難しいと感じました(実際、設定の整合性が合わせるために、精密な作業が求められる)。しかしRESTful APIで認可を包括的に設定したい、という需要は少なく無いでしょうし(私も欲しい)、まだ定番の方式やプロダクトも無い状況なので、早く何か欲しいところではあります。

また、今回はAPI Gatewayの話(≒認可の話)のため、APIの認証の話が出来ませんでした(書く予定はあったのですが、話がまとめられませんでした)。API Gatewayは、認証は完了している想定で話が進みますが、現実の問題では認証もやらなくてはならないので、無視できない問題です。機会があれば書きたいと思います - まぁ、多分来年のアドベントかな!

60
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
29