2日目: Go 言語による Web アプリケーション
Web
はてなのエンジニアは九分九厘 Web アプリケーションに関わっている
サーバサイド
クライアントサイド
(いわゆる) Web フロントエンド
スマホアプリ
Web?
URL に HTTP でアクセスすると降ってくる HTML をブラウザーが描画する
URL とは?
HTTP とは?
HTML とは? ブラウザーとは?
詳しくは 5 日目
HTTP
HyperText Transfer Protocol
Web ブラウザと Web サーバとの間の通信プロトコル
リクエストとレスポンスのデータの形に関する決まりごと
code:HTTP リクエスト/レスポンスの例
GET / HTTP/1.1 # Request line (method SP request-target SP HTTP-version)
Host: www.example.com # Request headers
User-Agent: curl/7.54.0
Accept: */*
< HTTP/1.1 200 OK # Status line (HTTP-version SP status-code SP reason-phrase)
< Accept-Ranges: bytes # Response headers
< Content-Type: text/html; charset=UTF-8
< Date: Mon, 19 Aug 2019 11:56:54 GMT
< Server: ECS (sec/96EE)
< Content-Length: 1270
<!doctype html> # Response body
<html>
<head>
<title>Example Domain</title>
...
HTTP 概要
歴史
URL
HTTP リクエスト
メソッド
パス
ヘッダ
ボディ
HTTP レスポンス
ステータスコード
ヘッダ
ボディ
認証
進んだ話題
HTTP の歴史
HTTP/0.9 (1991年)
GET のみ・ステータスコードもなし
メソッド・レスポンスヘッダ・ステータスコードの登場
Host ヘッダ、新しいメソッド
バイナリフォーマット
接続の多重化・パイプライン化
ヘッダの圧縮
QUIC のトランスポートプロトコル + HTTP/2 っぽいデータ構造でさらなる高速化 RFC
HTTP: The Hypertext Transfer Protocol (HTTP) is a stateless application-level protocol for distributed, collaborative, hypertext information systems.
Uniform Resource Locator: リソースの住所, アドレス
例: http://www.example.com:80/index.html?id=sample#foo
スキーム: http, https, mailto, file, telnet, ……
ホスト名: www
ドメイン名: example.com
ポート番号: :80 (http), :443 (https)
パス: /index.html
クエリ文字列: ?id=sample
フラグメント #foo
URL のエンコーディング
スペースや UTF-8 文字など、URL で許されない文字を URL で表現するためのエンコーディング
国際化ドメイン名 (IDN) で使われるエンコーディング
必ず xn-- から始まる
A Uniform Resource Identifier (URI) is a compact sequence of characters that identifies an abstract or physical resource.
URL (Uniform Resource Locator) / URN (Uniform Resource Name) を包含する一般的な概念 (URN は名前で、URL は住所)
URL と URI は区別されていたが、W3C によるともう気にしなくてよい
URI and IRI are just confusing.
言語の標準 (デファクト) ライブラリ
URI の例 (RFC3986)
ftp://ftp.is.co.za/rfc/rfc1808.txt
http://www.ietf.org/rfc/rfc2396.txt
mailto:John.Doe@example.com
tel:+1-816-555-1212
telnet://192.0.2.16:80/
urn:oasis:names:specification:docbook:dtd:xml:4.1.2 (URN)
HTTP リクエスト
メソッド
パス
ヘッダ
ボディ
同じリソースに対して異なるメソッドを使いわけて、様々な操作を表すことができる
The request method token is the primary source of request semantics; ...
GET: リソースを転送する
HEAD: ヘッダのみ要求する
POST: リクエストのペイロードに対してリソース固有の処理を行う
PUT: リクエストのペイロードで対象リソースを置き換える
DELETE: 対象リソースを削除する
その他: CONNECT, OPTIONS, TRACE, PATCH (RFC5789) 5.1. Controls; リクエストの取り扱い方を示す
Host, Cache-Control, ……
5.2. Conditionals; 対象リソースの状態に基づいてレスポンスを切り替える
If-Match, If-Modified-Since, ……
Accept, Accept-Charset, Accept-Encoding, Accept-Language
5.4. Authentication Credentials
5.5. Request Context
Form, Referer, User-Agent, ……
リクエスト・メッセージ、ペイロード
データの新規作成・更新
データ形式
HTML フォーム: x-www-form-urlencoded, multipart/form-data
JSON: application/json
XML: application/xml
例: HTTP リクエスト
code:HTTP リクエスト
Trying 93.184.216.34...
TCP_NODELAY set
Connected to example.com (93.184.216.34) port 80 (#0)
GET / HTTP/1.1
Host: example.com
User-Agent: curl/7.54.0
Accept: */*
HTTP レスポンス
ステータスコード
ヘッダ
ボディ
レスポンスの意味を表す三桁の整数
一桁目で分類
1xx Informational: リクエスト受付・処理を続行
2xx Successful: リクエスト受理
3xx Redirection: リダイレクト
4xx Client Error: リクエストに誤り
5xx Server Error: サーバーの処理失敗
クライアントは必ずしも全てのステータスコードを解釈できる必要はないが、未知のステータスコードは x00 と解釈することとされている (RFC7221 6.)
treat an unrecognized status code as being equivalent to the x00 status code of that class
使われがちなステータスコード
200: OK
301: Moved Permanently
302: Found
303: See Other
400: Bad Request
401: Unauthorized
403: Forbidden
404: Not Found
500: Internal Server Error
503: Service Unavailable
サーバーの状態や、対象リソースの情報を返す
7.1. Control Data
Age, Cache-Control, Expires
Location, Retry-After
7.2. Validator Header Fields
Etag, Last-Modified
7.3. Authentication Challenges
WWW-Authenticate, Proxy-Authenticate
7.4. Response Context
Server
セキュリティー関連のヘッダ
X-XSS-Protection: XSS に対するフィルター機能を強制
X-Frame-Options: iframe によりアクセスできないように制御
X-Content-Type-Options: Content-Typeを無視したスクリプトの実行を抑制
Content-Security-Policy (CSP): 実行するスクリプトの制限
Strict-Transport-Security (HSTS): 次回以降 https でのリクエストを強制
詳しくは 4 日目
リクエストに対応するリソースの内容や、処理の結果を返す。
HTML 文章
JSON
XML
JavaScript, CSS, 画像, 動画, ……
例: HTTP レスポンス
code: HTTP レスポンス
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: max-age=604800
< Content-Type: text/html
< Date: Sun, 05 Aug 2018 11:21:44 GMT
< Etag: "1541025663"
< Expires: Sun, 12 Aug 2018 11:21:44 GMT
< Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
< Vary: Accept-Encoding
< X-Cache: HIT
< Content-Length: 1270
<
<!doctype html>
<html>
<head>
<title>Example Domain</title>
リクエスト・レスポンス まとめ
HTTP リクエスト
code:HTTP: GET リクエスト
GET / HTTP/1.1 # Request line
Host: example.com # Headers
Accept: */*
HTTP リクエスト (POST)
code:HTTP: POST リクエスト
POST /post HTTP/1.1 # Request line
Host: httpbin.org # Headers
Content-Type: application/json
Content-Length: 21
{ "sample": "hello" } # Request body
HTTP レスポンス
code:HTTP レスポンス
HTTP/1.1 200 OK # Status line
Cache-Control: max-age=604800 # Headers
Content-Type: text/html
Date: Sun, 05 Aug 2018 12:58:22 GMT
Server: ECS (oxr/8313)
Content-Length: 1270
<!doctype html> # Response Body
<html>
<head>
<title>Example Domain</title>
HTTP リクエスト・レスポンスの余談
HTTP/1.0 のメッセージフォーマットはメール (RFC822) を参考にしている 認証
アクセスしたユーザーが誰なのか
一方で、HTTP プロトコルは状態を持たない
状態を持たないプロトコルの上で、状態を扱うにはどうするか
Basic 認証
user:password フォーマットの文字列を base64 エンコードし、Authorization ヘッダに入れてリクエストする
code:BASIC 認証の例
GET /basic-auth/sample/passwd HTTP/1.1
Host: httpbin.org
Authorization: Basic c2FtcGxlOnBhc3N3ZA==
HTTP/1.1 200 OK
Connection: keep-alive
Server: gunicorn/19.9.0
Date: Sun, 12 Aug 2018 15:19:38 GMT
Content-Type: application/json
Content-Length: 49
{
"authenticated": true,
"user": "sample"
}
状態を持つ「セッション」を管理できるようにする仕組み
Set-Cookie レスポンスヘッダ
サーバーがこのヘッダに情報を乗せる
クライアントがこの情報を保存する
Expires, Max-Age, Domain, Path, Secure, HttpOnly 属性
Cookie リクエストヘッダ
同じドメインにリクエストを送信する際に、保存している情報をヘッダとして送信する
Cookie を使った認証パターン
ログイン
ユーザーに紐づくトークンを発行し、Set-Cookieで返す
クライアントはトークンをローカルに保存
認証
Cookieのトークンからユーザーを解決する
ユーザーが解決できなかったり、トークンが無効なとき認証失敗
ログアウト
値を空、Expiresを昔にしてSet-Cookieを返す
クライアントは該当トークンを消す
Cookie の注意事項
HTTP 通信のなかでは平文で送受信される
Secure 属性をつけると、HTTPS 接続時にだけ送信される
Cookie の値は、ユーザーが意図的に閲覧・変更・削除できる
クライアントはSet-Cookieを無視できる
最大容量は "だいたい" 4KB
「Cookie の歴史は Web セキュリティーの歴史」
Cookie 以外でブラウザに情報を保存する
JavaScript
HTTP の進んだ話題
HTTPS
HTTP/2
JWT
HTTPS: HTTP over TLS
SSL?
クライアント証明書を使うと認証
時代の潮流は HTTPS
Let's Encrypt の登場や CDN サービスなどエコシステムの充実
ビジネス、SEO 的な要求
弊社の事例
興味があれば苦労話を聞いてみよう
速度の改善を主目的として策定された HTTP のバージョン
リクエストとレスポンスの多重化
ヘッダの圧縮: HPACK
サーバープッシュ
ストリームの優先順位と重み
JSON Web Token /dʒɒt/ (ジョット)
JSON をエンコードした URL-safe な署名付きのトークン
header.payload.signature
header: 署名アルゴリズム・トークンタイプの Base64URL
{ "alg": "HS256", "typ": "JWT" }
payload: 独自の情報 + クレーム情報 の Base64URL
signature: 改竄されていないことを確かめるための署名
ここで休憩タイム!
5分間休憩にしましょう
Go 言語による Web アプリケーション
Web アプリケーション
リポジトリ層: repository
DB とのデータのやり取り
サービス層: service
アプリケーションのロジック
ウェブ層: web
URL ルーティング、認証、テンプレートのレンダリング
リポジトリ層
DB とのデータのやり取り (SQL の詳細は明日)
code: sample_repository.go
func (r *repository) FindUserByID(id uint64) (*User, error) {
var user model.User
err := r.db.Get(
&user,
SELECT id,name FROM user WHERE id = ? LIMIT 1,
id,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, userNotFoundError
}
return nil, err
}
return &user, nil
}
サービス層
アプリケーションのロジック
App は Repository を持つ
リポジトリの関数を使って処理を記述する
code: sample_app.go
type bookmarkApp struct {
repo repository.Repository
}
func (app *bookmarkApp) FindUserByID(userID uint64) (*model.User, error) {
return app.repo.FindUserByID(userID)
}
ウェブ層
ルーティング
リクエストボディ
認証
テンプレート
「ミドルウェア」
ルーティング
処理したい URL + HTTP メソッド と、リクエストを処理する関数 (コントローラ、ハンドラ) の組み合わせの管理
どの URL でどの処理を行う、というののマッピング
Echo#Context にはリクエストに関する情報が全部入っている
code: main.go
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World, " + c.RealIP() + "!")
})
e.Start(":8080")
リクエストボディの処理: フォーム
HTML のフォームの値の処理
code: (go)
// エラー処理は省略しています
func (s *server) signupHandler(c echo.Context) error {
params := new(struct {
Name string form:name
Password string form:password
})
c.Bind(params)
s.app.CreateNewUser(params.Name, params.Password)
...
}
リクエストボディの処理: JSON
JSON
code: (go)
func Handler(c echo.Context) error {
var d SomeData
if err := json.NewDecoder(c.Request().Body).Decode(&d); err != nil {
return c.String(http.StatusInternalServerError, "Malformed JSON")
}
fmt.Printf("%+v\n", d)
}
Cookie による認証: サインアップ
code:go
// name と password が POST されてやってくるエンドポイントのハンドラー
// エラー処理は省略しています
func (s *server) signupHandler(c echo.Context) error {
params := new(struct {
Name string form:name
Password string form:password
})
c.Bind(params)
// go-Intern-Bookmark の CreateNewUser を見てみよう
s.app.CreateNewUser(params.Name, params.Password)
user, _ := s.app.FindUserByName(params.Name)
expiresAt := time.Now().Add(sessionLifespan)
token, _ := s.app.CreateNewToken(user.ID, expiresAt)
c.SetCookie(&http.Cookie{
Name: sessionKey,
Value: token,
Expires: expiresAt,
})
return c.Redirect(http.StatusFound, "/")
}
サインアップ
パスワードの保存方法
生のパスワードを保存してはまずい
code:go
func (app *bookmarkApp) CreateNewUser(name string, password string) (err error) {
if name == "" {
return errors.New("empty user name")
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
return app.repo.CreateNewUser(name, string(passwordHash))
}
Cookie による認証
code: (go)
const sessionKey = "BOOKMARK_SESSION"
func (s *server) findVisitor(c echo.Context) *model.User {
cookie, _ := c.Cookie(sessionKey)
if cookie.Value != "" {
if user, _ := s.app.FindUserByToken(cookie.Value); user != nil {
return user
}
}
return nil
}
HTML テンプレート
表示する HTML の内容をリクエストごとにカスタマイズしたい
ログインしてなかったらログインボタンを表示する
ユーザーページを開いたらそのユーザーの情報を表示する
……
Echo 自体のサポートは素朴、やり方いろいろ
テンプレートファイルの読み込みとコンパイル
テンプレート
code: (html)
{{if .User}}
あなたのユーザー名: {{.User.Name}}
<form action="/signout" method="POST">
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<input type="submit" value="ログアウト"/>
</form>
{{else}}
<a href="/signup">ユーザー登録</a>
<a href="/signin">ログイン</a>
{{end}}
「ミドルウェア」
Web アプリケーションにおける様々な処理を「ミドルウェア」として表現する
認証
リクエスト・レスポンスのロギング
レスポンスヘッダ
さまざまな「ミドルウェア」
Echo におけるミドルウェア
素の Go 言語では: http.Handler を受け取って http.Handler を返す関数
Echo#Context を受け取り、なんらかの処理を行う
code:middleware_example.go
func (s *server) exampleHeaderMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("X-Echo-Middleware", "is working")
return next(c)
}
}
}
レイヤーの結合
抽象に依存させよ
それぞれの層での話題:
SQL を発行するのはリポジトリ層の責務である
サービス層が直接 SQL を発行できないようにするにはどうすればいいだろうか?
サービス層は、リポジトリの操作を組み合わせて実装したい
「サービス」は「リポジトリ」を持っている?
リポジトリ層以外は、リポジトリの具体を知りたくない
接続先が MySQL か PostgreSQL か、RDBMS ですらないかもしれない
code: (go)
// 抽象を公開
type Repository interface {
FindUserByID(id uint64) (*model.User, error)
}
// 非公開
type repository struct {
db *sqlx.DB
}
func (r *repository) FindUserByID(id uint64) (*model.User, error) {
}
// サービスは interface に依存
type bookmarkApp struct {
repo Repository
}
テスト
書いたコードが意図した挙動をしているか
コーナーケースが考慮されているか
動いてはいけないケースをきちんと弾いているか
ライブラリーのバージョンを上げても、アプリケーションの挙動が変わらないこと
昨日の自分は赤の他人
テストで確認すること
正常系
正しい条件で正しく動くこと
異常系
おかしな条件のときに意図したエラーを吐くこと
境界条件・極端な条件
ゼロや空の値・配列、巨大なデータ
テストよもやま話
機能試験・性能試験
ユニットテスト・統合テスト
カバレッジ
命令網羅: C0
分岐網羅: C1
条件網羅: C2
TDD (テスト駆動開発)
課題
必須課題・発展研究
必須課題: Webアプリケーション
日記 Web アプリケーション go-Intern-Diary のユーザー登録・ログイン機能を作ってください
ユーザーに関連するテーブルを作りましょう: db/
ユーザーに関連するリポジトリ層・サービス層を作りましょう: repository/, service/
(オプション) サービス層はテストを書きましょう
Echo を使ってウェブ層を作りましょう: web/
トップページのテンプレートを作り、ブラウザーでアクセスできるようにしましょう
ログイン・ログアウト機能を作りましょう
/signup でユーザー登録できる
/login でログインできる
ログアウトできる
go-Intern-Bookmark のコピペでよいですが、コンパイルできることを確認、コミットしながら進めましょう
参考情報:
発展研究: HTTP
全部をやる必要はありません!! ★ マークつきのもののうち、興味があるものについて回答を提出してもらえれば大丈夫です
HTTP (+ curl) の基礎
curl -v -I http://httpbin.org を試してみよう
リクエストメソッド、URL、ヘッダ、レスポンスのステータス、ヘッダを確認しよう
-I なしで curl を実行してみよう
サーバーで使われている技術についてなにか分かるだろうか?
https://b.hatena.ne.jp/ や https://hatenablog.com/ にリクエストしてみよう
Content-Lengthの値は正しいだろうか?
ブラウザー・telnet
ブラウザーを使って、リクエストヘッダ、レスポンスヘッダを確認してみよう
ブラウザーが保持している Cookie を確認してみよう
304 ステータスが返ってくるエンドポイントの仕組みを理解しよう
telnetコマンドを使って、GET や POST など色々な HTTP リクエストを送ってみよう
Hostヘッダはなぜ必要なのでしょうか?
httpbin.orgの IP アドレスを dig や whois で調べてください
どういうことがわかりますか?
そのIPアドレスに対して curl するとどうなりますか?
Hostヘッダをつけるとどうなりますか?
メソッド
curl -v http://httpbin.org/post を実行して、レスポンスの意味を理解しよう
この URL へのリクエストに対して、ステータスコード 200 が返ってくるようにするにはどうすればいいだろうか?
リクエストヘッダ
http://httpbin.org/headers にリクエストを送ってみよう
リクエストを送るときに User-Agentを変更してみよう
リクエストボディ
http://httpbin.org/post に name: John, age: 20 というデータを application/x-www-form-urlencoded で送信してください
{ "name": "John", "age": 20 } という JSON を送信してください
適当なテキストファイルを添付して multipart/form-data で送信してください
Basic 認証
http://httpbin.org/basic-auth/sample/passwd にリクエストを送ってみよう
ステータスコードを調べよう
この URL へのリクエストに対して、ステータスコード 200 が返ってくるようにするにはどうすればいいだろうか (curlの-uオプションを使わずにリクエストしてください)
Cookie
curl -v -I https://b.hatena.ne.jp/ を試してみよう
Set-Cookieに従ってCookieヘッダをつけてリクエストすると何が変わるだろうか?
Cookie の各属性について調べてみよう
★ Third-party cookie とはどういうものか調べよう
どのようなときに使われることが多いでしょうか?
Third-party cookie を利用しないと実現できない Web サービスの機能はどんなものでしょうか?
リダイレクト
http://httpbin.org/redirect/1 にリクエストを送ってみよう。ステータスコードの意味や、リダイレクト先について調べてみよう
リダイレクトをフォローする curl のオプションを調べよう
そのオプションをつけるとどうなるだろうか?
二回リダイレクトするとどうなるだろうか?
curl のメッセージ Re-using existing connection! とはどういう意味だろうか?
★ 300 番台のステータスコードの違いについて調べよう
HTTPS
ブラウザーやコマンドラインで、https なウェブサイトの証明書情報を確認する方法を調べてみよう
発行者は?
いくつかのウェブサイトのものを眺めていてなにか気づいたことがありますか?
HSTS (HTTP Strict Transport Security) について調べてみよう
HTTP/2
example.com に対して、HTTP/1.1 と HTTP/2 とでそれぞれリクエストしたときの違いを確認しよう
ブラウザーの開発者ツールで HTTP/2 接続時のヘッダを見たときに、なにか気がつくことはありますか?
ヘッダの圧縮形式 HPACK の符号化方式について調べよう
ボディの圧縮に広く使われる gzip ではない理由はなぜでしょうか?
ストリーム、メッセージ、フレームの意味を調べよう
ストリームの優先順位と重みは、何のために必要なのでしょうか?
★ HTTP/2 によってレイテンシが改善する理由を 3 つ以上考えてみよう
HTTP/2 は HTTP/1.1 を置き換えるものではない、というのはどういう意味だろうか?
仕様が策定された経緯を調べてみよう
Web アプリケーションのセキュリティ
★ 今の状態で go-Intern-Diary を世にリリースするとどのような問題があるでしょうか?
X-Frame-Options: DENY を消すと挙動が変わることを確かめてください
このヘッダをつけていない Web アプリケーションに対する攻撃方法を調べましょう
X-Frame-Options: DENY などのセキュリティ系ヘッダー群を、Echo 標準添付のミドルウェアを用いて設定してみましょう
CSRF トークンの input (ここでは name="csrf_token") を消すとどうなるか確認してください
CSRF とは何の略でしょうか?
CSRF 対策をしていない Web アプリケーションに対する攻撃方法を調べましょう
★ ログインパスワードを安全に保存する方法について調べましょう
bcrypt の利点を答えてください
PBKDF2 や scrypt との違いを調べましょう
補足
コメントを書きましょう!!!
自分のため
講師やメンターなど、他の人のため
意図
複雑だけど高速化のための工夫です
どれくらい自信があるコードなのか
いろいろ考えた結果の苦肉の策
こう書いたらなぜか動いた
……
文脈
参考にした Web ページ
自慢
シュッとしてるので見てくれ!!!
逃げ
あんまり自信がないところ
// TODO あとでやる
Credit