├── .gitignore ├── LICENSE ├── README.md └── webserver ├── auth.go ├── data └── users.json ├── handler.go ├── model └── user.go ├── public ├── css │ └── .gitkeep ├── img │ └── .gitkeep └── js │ └── .gitkeep ├── server.go ├── session ├── cookie.go ├── manager.go └── manager_local.go ├── setting └── setting.go ├── static.go ├── template.go └── templates ├── admin.html ├── admin_users.html ├── error.html ├── index.html ├── layout.html ├── login.html └── user.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 15 | .glide/ 16 | 17 | *.log 18 | debug 19 | 20 | .vscode/ 21 | !.gitkeep 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Yoshinoya Ussie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-website-sample 2 | Go言語でWebサイトを作ってみるサンプルです。フレームワークは 3 | Echo https://echo.labstack.com/ 4 | を使用しています。 5 | 6 | ## 概要 7 | 8 | WebサイトのサーバーサイドをGoで一通り作っていっています。 9 | 詳細につきましては以下のQiita記事を参照してください。 10 | 11 | ### Go言語でWebサイトを作ってみる: 12 | 13 | * Hello World的な http://qiita.com/y_ussie/items/ca8dc5e423eec318a436 14 | * リクエストパラメータの扱い http://qiita.com/y_ussie/items/2d397e70bfc38f75ca51 15 | * セッションデータストアを作る http://qiita.com/y_ussie/items/b1db86b0b54ec69bb928 16 | * Cookieを使用したセッション管理 http://qiita.com/y_ussie/items/00e542cb3531b48fd21a 17 | * ひとまずコード整理 http://qiita.com/y_ussie/items/12bb4fd8cefb740581f8 18 | * ユーザー情報をJSONから読み込んで参照してみる http://qiita.com/y_ussie/items/8704ce209704bf191e63 19 | * ログインしたユーザーしか見られないページを作ってみる http://qiita.com/y_ussie/items/45d916f741e12c4ec9b7 20 | * 管理者のみ見られるページを作ってみる http://qiita.com/y_ussie/items/0052c83c9ec75b06bb6c 21 | 22 | ## コード全体構成 23 | 24 | ``` 25 | / 26 | └─webserver 27 |    │ auth.go     認証関連の処理 28 |    │ handler.go リクエストハンドラの定義 29 | │ server.go サーバーのメイン処理 30 | │ static.go 静的ファイルパスの定義 31 | │ template.go HTMLテンプレートの定義 32 |    ├─data       JSONファイルなど 33 |    │ users.json  ユーザー情報のJSONファイル 34 |    ├─model      データモデルとアクセサ 35 |    │ user.go    ユーザー情報のモデルとアクセサ 36 |    ├─public     静的ファイル 37 | │ ├─css CSSファイル 38 | │ ├─img 画像ファイル 39 | │ └─js JavaScriptファイル 40 | ├─session セッション関連の処理 41 | │ cookie.go セッションCookie関連 42 | │ manager.go セッションデータ管理(公開関数) 43 | │ manager_local.go セッションデータ管理(非公開関数) 44 | ├─setting 設定関連の処理 45 | │ setting.go 設定データの定義 46 | └─templates HTMLテンプレート 47 |            admin.html        (管理者)ホーム画面 48 |            admin_users.html  (管理者)ユーザー一覧画面 49 |            error.html       エラーメッセージ画面 50 | index.html index画面 51 |            layout.html       共通レイアウト 52 |            login.html        ログイン画面 53 |            user.html         ユーザー情報の表示画面 54 | ``` 55 | -------------------------------------------------------------------------------- /webserver/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "./model" 8 | "./session" 9 | "github.com/labstack/echo" 10 | ) 11 | 12 | // auth.goが返すエラーの定義 13 | var ( 14 | ErrorInvalidUserID = errors.New("Invalid UserID") 15 | ErrorInvalidPassword = errors.New("Invalid Password") 16 | ErrorNotLoggedIn = errors.New("Not Logged In") 17 | ) 18 | 19 | // UserLogin はユーザーログイン時の処理を行います。 20 | func UserLogin(c echo.Context, userID string, password string) error { 21 | users, err := userDA.FindByUserID(userID, model.FindFirst) 22 | if err != nil { 23 | return err 24 | } 25 | user := &users[0] 26 | encodePassword := model.EncodeStringMD5(password) 27 | if user.Password != encodePassword { 28 | return ErrorInvalidPassword 29 | } 30 | sessionID, err := sessionManager.Create() 31 | if err != nil { 32 | return err 33 | } 34 | err = session.WriteCookie(c, sessionID) 35 | if err != nil { 36 | return err 37 | } 38 | sessionStore, err := sessionManager.LoadStore(sessionID) 39 | if err != nil { 40 | return err 41 | } 42 | sessionData := map[string]string{ 43 | "user_id": userID, 44 | } 45 | sessionStore.Data = sessionData 46 | err = sessionManager.SaveStore(sessionID, sessionStore) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // UserLogout はユーザーログアウト時の処理を行います。 55 | func UserLogout(c echo.Context) error { 56 | sessionID, err := session.ReadCookie(c) 57 | if err != nil { 58 | return err 59 | } 60 | err = sessionManager.Delete(sessionID) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // CheckUserID は指定されたユーザーIDでログインしているか確認します。 69 | func CheckUserID(c echo.Context, userID string) error { 70 | sessionID, err := session.ReadCookie(c) 71 | if err != nil { 72 | return err 73 | } 74 | sessionStore, err := sessionManager.LoadStore(sessionID) 75 | if err != nil { 76 | return err 77 | } 78 | sessionUserID, ok := sessionStore.Data["user_id"] 79 | if !ok { 80 | return ErrorNotLoggedIn 81 | } 82 | if sessionUserID != userID { 83 | return ErrorInvalidUserID 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // CheckRole は指定された権限を持ったユーザーでログインしているか確認します。 90 | func CheckRole(c echo.Context, role model.Role) (bool, error) { 91 | sessionID, err := session.ReadCookie(c) 92 | if err != nil { 93 | return false, err 94 | } 95 | sessionStore, err := sessionManager.LoadStore(sessionID) 96 | if err != nil { 97 | return false, err 98 | } 99 | sessionUserID, ok := sessionStore.Data["user_id"] 100 | if !ok { 101 | return false, ErrorNotLoggedIn 102 | } 103 | haveRole, err := CheckRoleByUserID(sessionUserID, role) 104 | return haveRole, nil 105 | } 106 | 107 | // CheckRoleByUserID はユーザーが指定された権限を持っているか確認します。 108 | func CheckRoleByUserID(userID string, role model.Role) (bool, error) { 109 | users, err := userDA.FindByUserID(userID, model.FindFirst) 110 | if err != nil { 111 | return false, err 112 | } 113 | user := &users[0] 114 | for _, v := range user.Roles { 115 | if v == role { 116 | return true, nil 117 | } 118 | } 119 | 120 | return false, nil 121 | } 122 | 123 | // MiddlewareAuthAdmin は管理者権限を持ったユーザーのみが参照できる 124 | // ページに適用するMiddlewareです。 125 | func MiddlewareAuthAdmin(next echo.HandlerFunc) echo.HandlerFunc { 126 | return func(c echo.Context) error { 127 | isAdmin, err := CheckRole(c, model.RoleAdmin) 128 | if err != nil { 129 | c.Echo().Logger.Debugf("Admin Page Role Error. [%s]", err) 130 | isAdmin = false 131 | } 132 | if !isAdmin { 133 | msg := "管理者でログインしていません。" 134 | return c.Render(http.StatusOK, "error", msg) 135 | } 136 | return next(c) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /webserver/data/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "806d5832-e253-40ca-be80-a00878898c37", 4 | "user_id": "a-chan", 5 | "password": "5f4dcc3b5aa765d61d8327deb882cf99", 6 | "full_name": "Nishiwaki Ayaka", 7 | "roles": ["user"] 8 | }, 9 | { 10 | "id": "e7bdce91-c9bb-4a29-91f8-750ad4b18bfa", 11 | "user_id": "kashiyuka", 12 | "password": "5f4dcc3b5aa765d61d8327deb882cf99", 13 | "full_name": "Kashino Yuka", 14 | "roles": ["user"] 15 | }, 16 | { 17 | "id": "b87c826e-429b-4f50-aa35-b99f11a40b08", 18 | "user_id": "nocchi", 19 | "password": "5f4dcc3b5aa765d61d8327deb882cf99", 20 | "full_name": "Omoto Ayano", 21 | "roles": ["user"] 22 | }, 23 | { 24 | "id": "c7a70e10-0c89-48ab-9b30-43fad782cbe4", 25 | "user_id": "admin", 26 | "password": "5f4dcc3b5aa765d61d8327deb882cf99", 27 | "full_name": "Administrator", 28 | "roles": ["user","admin"] 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /webserver/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "./model" 7 | 8 | "github.com/labstack/echo" 9 | ) 10 | 11 | // ルーティングに対応するハンドラを設定します。 12 | func setRoute(e *echo.Echo) { 13 | e.GET("/", handleIndexGet) 14 | e.GET("/login", handleLoginGet) 15 | e.POST("/login", handleLoginPost) 16 | e.POST("/logout", handleLogoutPost) 17 | e.GET("/users/:user_id", handleUsers) 18 | e.POST("/users/:user_id", handleUsers) 19 | 20 | // 管理者のみが参照できるページ 21 | admin := e.Group("/admin", MiddlewareAuthAdmin) 22 | admin.GET("", handleAdmin) 23 | admin.POST("", handleAdmin) 24 | admin.GET("/users", handleAdminUsersGet) 25 | } 26 | 27 | // GET:/ 28 | func handleIndexGet(c echo.Context) error { 29 | return c.Render(http.StatusOK, "index", "world") 30 | } 31 | 32 | // GET:/users/:user_id 33 | // POST:/users/:user_id 34 | func handleUsers(c echo.Context) error { 35 | userID := c.Param("user_id") 36 | err := CheckUserID(c, userID) 37 | if err != nil { 38 | c.Echo().Logger.Debugf("User Page[%s] Role Error. [%s]", userID, err) 39 | msg := "ログインしていません。" 40 | return c.Render(http.StatusOK, "error", msg) 41 | } 42 | users, err := userDA.FindByUserID(c.Param("user_id"), model.FindFirst) 43 | if err != nil { 44 | return c.Render(http.StatusOK, "error", err) 45 | } 46 | user := users[0] 47 | return c.Render(http.StatusOK, "user", user) 48 | } 49 | 50 | // GET:/admin 51 | // POST:/admin 52 | func handleAdmin(c echo.Context) error { 53 | return c.Render(http.StatusOK, "admin", nil) 54 | } 55 | 56 | // GET:/admin/users 57 | func handleAdminUsersGet(c echo.Context) error { 58 | users, err := userDA.FindAll() 59 | if err != nil { 60 | return c.Render(http.StatusOK, "error", err) 61 | } 62 | return c.Render(http.StatusOK, "admin_users", users) 63 | } 64 | 65 | // GET:/login 66 | func handleLoginGet(c echo.Context) error { 67 | return c.Render(http.StatusOK, "login", nil) 68 | } 69 | 70 | // POST:/login 71 | func handleLoginPost(c echo.Context) error { 72 | userID := c.FormValue("userid") 73 | password := c.FormValue("password") 74 | err := UserLogin(c, userID, password) 75 | if err != nil { 76 | c.Echo().Logger.Debugf("User[%s] Login Error. [%s]", userID, err) 77 | msg := "ユーザーIDまたはパスワードが誤っています。" 78 | data := map[string]string{"user_id": userID, "password": "", "msg": msg} 79 | return c.Render(http.StatusOK, "login", data) 80 | } 81 | // ログインしたユーザーが管理者かチェックする 82 | isAdmin, err := CheckRoleByUserID(userID, model.RoleAdmin) 83 | if err != nil { 84 | c.Echo().Logger.Debugf("Admin Role Check Error. [%s]", userID, err) 85 | isAdmin = false 86 | } 87 | if isAdmin { 88 | // 管理者でログインした場合には管理者のホーム画面に遷移する 89 | c.Echo().Logger.Debugf("User is Admin. [%s]", userID) 90 | return c.Redirect(http.StatusTemporaryRedirect, "/admin") 91 | } 92 | return c.Redirect(http.StatusTemporaryRedirect, "/users/"+userID) 93 | } 94 | 95 | // POST:/logout 96 | func handleLogoutPost(c echo.Context) error { 97 | err := UserLogout(c) 98 | if err != nil { 99 | c.Echo().Logger.Debugf("User Logout Error. [%s]", err) 100 | return c.Render(http.StatusOK, "login", nil) 101 | } 102 | msg := "ログアウトしました。" 103 | data := map[string]string{"user_id": "", "password": "", "msg": msg} 104 | return c.Render(http.StatusOK, "login", data) 105 | } 106 | -------------------------------------------------------------------------------- /webserver/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "encoding/json" 7 | "errors" 8 | "io" 9 | "io/ioutil" 10 | 11 | "github.com/labstack/echo" 12 | ) 13 | 14 | // User はユーザーの情報を表します。 15 | type User struct { 16 | ID ID `json:"id"` 17 | UserID string `json:"user_id"` 18 | Password StringMD5 `json:"password"` 19 | FullName string `json:"full_name"` 20 | Roles []Role `json:"roles"` 21 | } 22 | 23 | // Copy は情報のコピーを行います。 24 | func (u *User) Copy(f *User) { 25 | u.ID = f.ID 26 | u.UserID = f.UserID 27 | u.Password = f.Password 28 | u.FullName = f.FullName 29 | u.Roles = make([]Role, len(f.Roles)) 30 | copy(u.Roles, f.Roles) 31 | } 32 | 33 | // UserDataAccessor はユーザーの情報を操作するAPIを提供します。 34 | type UserDataAccessor struct { 35 | stopCh chan struct{} 36 | commandCh chan command 37 | } 38 | 39 | // ID は情報を一意に識別するためのIDです。 40 | type ID string 41 | 42 | // StringMD5 はMD5ハッシュ化された文字列です。 43 | type StringMD5 string 44 | 45 | // Role はユーザーの権限を表します。 46 | type Role string 47 | 48 | // ユーザー権限の定義 49 | const ( 50 | RoleAdmin Role = "admin" 51 | RoleUser Role = "user" 52 | ) 53 | 54 | // Start はAccessorの開始を行います。 55 | func (a *UserDataAccessor) Start(echo *echo.Echo) error { 56 | e = echo 57 | users = make(map[ID]User) 58 | if err := a.decodeJSON(); err != nil { 59 | return err 60 | } 61 | go a.mainLoop() 62 | return nil 63 | } 64 | 65 | // Stop はAccessorの停止を行います。 66 | func (a *UserDataAccessor) Stop() { 67 | a.stopCh <- struct{}{} 68 | } 69 | 70 | // FindAll はユーザーを全件検索します。 71 | func (a *UserDataAccessor) FindAll() ([]User, error) { 72 | respCh := make(chan response, 1) 73 | defer close(respCh) 74 | req := []interface{}{} 75 | cmd := command{commandFindAll, req, respCh} 76 | a.commandCh <- cmd 77 | resp := <-respCh 78 | var res []User 79 | if resp.err != nil { 80 | e.Logger.Debugf("User Find Error. [%s]", resp.err) 81 | return res, resp.err 82 | } 83 | if res, ok := resp.result[0].([]User); ok { 84 | return res, nil 85 | } 86 | e.Logger.Debugf("User Find Error. [%s]", ErrorOther) 87 | return res, ErrorOther 88 | } 89 | 90 | // FindByUserID はUserIDでユーザーを検索します。 91 | func (a *UserDataAccessor) FindByUserID(reqUserID string, option FindOption) ([]User, error) { 92 | respCh := make(chan response, 1) 93 | defer close(respCh) 94 | req := []interface{}{reqUserID, option} 95 | cmd := command{commandFindByUserID, req, respCh} 96 | a.commandCh <- cmd 97 | resp := <-respCh 98 | var res []User 99 | if resp.err != nil { 100 | e.Logger.Debugf("User[UserID=%s] Find Error. [%s]", reqUserID, resp.err) 101 | return res, resp.err 102 | } 103 | if res, ok := resp.result[0].([]User); ok { 104 | return res, nil 105 | } 106 | e.Logger.Debugf("User[UserID=%s] Find Error. [%s]", reqUserID, ErrorOther) 107 | return res, ErrorOther 108 | } 109 | 110 | // EncodeStringMD5 は、MD5エンコードした文字列を返します。 111 | func EncodeStringMD5(str string) StringMD5 { 112 | h := md5.New() 113 | io.WriteString(h, str) 114 | encodeStr := hex.EncodeToString(h.Sum(nil)) 115 | res := StringMD5(encodeStr) 116 | 117 | return res 118 | } 119 | 120 | // FindOption は検索時のオプションを定義します。 121 | type FindOption int 122 | 123 | // 検索時のオプション 124 | const ( 125 | FIndAll FindOption = iota // 全件検索 126 | FindFirst // 1件目のみ返す 127 | FindUnique // 結果が1件のみでない場合にはエラーを返す 128 | ) 129 | 130 | // DataAccessorが返す各エラーのインスタンスを生成します。 131 | var ( 132 | ErrorNotFound = errors.New("Not found") 133 | ErrorMultipleResults = errors.New("Multiple results") 134 | ErrorInvalidCommand = errors.New("Invalid Command") 135 | ErrorBadParameter = errors.New("Bad Parameter") 136 | ErrorNotImplemented = errors.New("Not Implemented") 137 | ErrorOther = errors.New("Other") 138 | ) 139 | 140 | func (a *UserDataAccessor) decodeJSON() error { 141 | // JSONファイル読み込み 142 | bytes, err := ioutil.ReadFile("data/users.json") 143 | if err != nil { 144 | return err 145 | } 146 | // JSONをデコードする 147 | var records []User 148 | if err := json.Unmarshal(bytes, &records); err != nil { 149 | return err 150 | } 151 | // 結果をmapにセットする 152 | for _, x := range records { 153 | users[x.ID] = x 154 | } 155 | return nil 156 | } 157 | 158 | // echoのインスタンス 159 | var e *echo.Echo 160 | 161 | // 情報をメモリ上に持つためのmap 162 | var users map[ID]User 163 | 164 | // コマンド種別の定義 165 | type commandType int 166 | 167 | const ( 168 | commandFindAll commandType = iota // 全件検索 169 | commandFindByID // IDで検索 170 | commandFindByUserID // UserIDで検索 171 | ) 172 | 173 | // コマンド実行のためのパラメータ 174 | type command struct { 175 | cmdType commandType 176 | req []interface{} 177 | responseCh chan response 178 | } 179 | 180 | // コマンド実行の結果 181 | type response struct { 182 | result []interface{} 183 | err error 184 | } 185 | 186 | // UserDataAccessor のメインループ処理 187 | func (a *UserDataAccessor) mainLoop() { 188 | a.stopCh = make(chan struct{}, 1) 189 | a.commandCh = make(chan command, 1) 190 | defer close(a.commandCh) 191 | defer close(a.stopCh) 192 | e.Logger.Info("model.UserDataAccessor:start") 193 | loop: 194 | for { 195 | // 受信したコマンドによって処理を振り分ける 196 | select { 197 | case cmd := <-a.commandCh: 198 | switch cmd.cmdType { 199 | // 全件検索 200 | case commandFindAll: 201 | results := []User{} 202 | for _, x := range users { 203 | user := User{} 204 | user.Copy(&x) 205 | results = append(results, user) 206 | } 207 | res := []interface{}{results} 208 | cmd.responseCh <- response{res, nil} 209 | break 210 | // IDで検索 211 | case commandFindByID: 212 | // 未実装 213 | cmd.responseCh <- response{nil, ErrorNotImplemented} 214 | break 215 | // UserIDで検索 216 | case commandFindByUserID: 217 | reqUserID, ok := cmd.req[0].(string) 218 | if !ok { 219 | cmd.responseCh <- response{nil, ErrorBadParameter} 220 | break 221 | } 222 | reqOption, ok := cmd.req[1].(FindOption) 223 | if !ok { 224 | cmd.responseCh <- response{nil, ErrorBadParameter} 225 | break 226 | } 227 | results := []User{} 228 | for _, x := range users { 229 | if x.UserID == reqUserID { 230 | user := User{} 231 | user.Copy(&x) 232 | results = append(results, user) 233 | if reqOption == FindFirst { 234 | break 235 | } 236 | } 237 | } 238 | if len(results) <= 0 { 239 | cmd.responseCh <- response{nil, ErrorNotFound} 240 | break 241 | } 242 | if reqOption == FindUnique && len(results) > 1 { 243 | cmd.responseCh <- response{nil, ErrorMultipleResults} 244 | break 245 | } 246 | res := []interface{}{results} 247 | cmd.responseCh <- response{res, nil} 248 | // それ以外(エラー) 249 | default: 250 | cmd.responseCh <- response{nil, ErrorInvalidCommand} 251 | } 252 | case <-a.stopCh: 253 | break loop 254 | } 255 | } 256 | e.Logger.Info("model.UserDataAccessor:stop") 257 | } 258 | -------------------------------------------------------------------------------- /webserver/public/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoshinoyaussie/golang-website-sample/34aff434bf3dbc5d81b5e07372e72580e68e406c/webserver/public/css/.gitkeep -------------------------------------------------------------------------------- /webserver/public/img/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoshinoyaussie/golang-website-sample/34aff434bf3dbc5d81b5e07372e72580e68e406c/webserver/public/img/.gitkeep -------------------------------------------------------------------------------- /webserver/public/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoshinoyaussie/golang-website-sample/34aff434bf3dbc5d81b5e07372e72580e68e406c/webserver/public/js/.gitkeep -------------------------------------------------------------------------------- /webserver/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "html/template" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "./model" 12 | "./session" 13 | "./setting" 14 | "github.com/labstack/echo" 15 | "github.com/labstack/echo/middleware" 16 | "github.com/labstack/gommon/log" 17 | ) 18 | 19 | // レイアウト適用済のテンプレートを保存するmap 20 | var templates map[string]*template.Template 21 | 22 | // セッション管理のインスタンス 23 | var sessionManager *session.Manager 24 | 25 | // データアクセサのインスタンス 26 | var userDA *model.UserDataAccessor 27 | 28 | func main() { 29 | // Echoのインスタンスを生成 30 | e := echo.New() 31 | 32 | // ログの出力レベルを設定 33 | // e.Logger.SetLevel(log.INFO) 34 | e.Logger.SetLevel(log.DEBUG) 35 | 36 | // テンプレートを利用するためのRendererの設定 37 | t := &Template{} 38 | e.Renderer = t 39 | 40 | // ミドルウェアを設定 41 | e.Use(middleware.Logger()) 42 | e.Use(middleware.Recover()) 43 | 44 | // 静的ファイルを配置するルーティングを設定 45 | setStaticRoute(e) 46 | 47 | // 各ルーティングに対するハンドラを設定 48 | setRoute(e) 49 | 50 | // セッション管理を開始 51 | sessionManager = &session.Manager{} 52 | sessionManager.Start(e) 53 | 54 | // データアクセサの開始 55 | userDA = &model.UserDataAccessor{} 56 | userDA.Start(e) 57 | 58 | // サーバーを開始 59 | go func() { 60 | if err := e.Start(setting.Server.Port); err != nil { 61 | e.Logger.Info("shutting down the server") 62 | } 63 | }() 64 | 65 | // 中断を検知したらリクエストの完了を10秒まで待ってサーバーを終了する 66 | // (Graceful Shutdown) 67 | quit := make(chan os.Signal) 68 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 69 | <-quit 70 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 71 | defer cancel() 72 | if err := e.Shutdown(ctx); err != nil { 73 | e.Logger.Info(err) 74 | e.Close() 75 | } 76 | 77 | // データアクセサの停止 78 | userDA.Stop() 79 | 80 | // セッション管理を停止 81 | sessionManager.Stop() 82 | 83 | // 終了ログが出るまで少し待つ 84 | time.Sleep(1 * time.Second) 85 | } 86 | 87 | // 初期化を行う 88 | func init() { 89 | // 設定の読み込み 90 | setting.Load() 91 | // HTMLテンプレートの読み込み 92 | loadTemplates() 93 | } 94 | -------------------------------------------------------------------------------- /webserver/session/cookie.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "../setting" 8 | "github.com/labstack/echo" 9 | ) 10 | 11 | // WriteCookie は、ブラウザのcookieにセッションIDを書き込みます。 12 | func WriteCookie(c echo.Context, sessionID ID) error { 13 | cookie := new(http.Cookie) 14 | cookie.Name = setting.Session.CookieName 15 | cookie.Value = string(sessionID) 16 | cookie.Expires = time.Now().Add(setting.Session.CookieExpire) 17 | c.SetCookie(cookie) 18 | return nil 19 | } 20 | 21 | // ReadCookie は、ブラウザのcookieからセッションIDを読み込みます。 22 | func ReadCookie(c echo.Context) (ID, error) { 23 | var sessionID ID 24 | cookie, err := c.Cookie(setting.Session.CookieName) 25 | if err != nil { 26 | return sessionID, err 27 | } 28 | sessionID = ID(cookie.Value) 29 | return sessionID, nil 30 | } 31 | -------------------------------------------------------------------------------- /webserver/session/manager.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | // ID はセッションを一意に識別するIDです。 11 | type ID string 12 | 13 | // Store はセッションデータと整合性トークンを保持する構造体です。 14 | type Store struct { 15 | Data map[string]string 16 | ConsistencyToken string 17 | } 18 | 19 | // Manager は Sessionの操作・管理を行います。 20 | type Manager struct { 21 | stopCh chan struct{} 22 | commandCh chan command 23 | stopGCCh chan struct{} 24 | } 25 | 26 | // Start は Managerの開始を行います。 27 | func (m *Manager) Start(echo *echo.Echo) { 28 | e = echo 29 | go m.mainLoop() 30 | time.Sleep(100 * time.Millisecond) 31 | go m.gcLoop() 32 | } 33 | 34 | // Stop は Managerの停止を行います。 35 | func (m *Manager) Stop() { 36 | m.stopGCCh <- struct{}{} 37 | time.Sleep(100 * time.Millisecond) 38 | m.stopCh <- struct{}{} 39 | } 40 | 41 | // Create は セッションの作成を行います。 42 | func (m *Manager) Create() (ID, error) { 43 | respCh := make(chan response, 1) 44 | defer close(respCh) 45 | cmd := command{commandCreate, nil, respCh} 46 | m.commandCh <- cmd 47 | resp := <-respCh 48 | var res ID 49 | if resp.err != nil { 50 | e.Logger.Debugf("Session Create Error. [%s]", resp.err) 51 | return res, resp.err 52 | } 53 | if res, ok := resp.result[0].(ID); ok { 54 | return res, nil 55 | } 56 | e.Logger.Debugf("Session Create Error. [%s]", ErrorOther) 57 | return res, ErrorOther 58 | } 59 | 60 | // LoadStore は データストアの読み出しを行います。 61 | func (m *Manager) LoadStore(sessionID ID) (Store, error) { 62 | respCh := make(chan response, 1) 63 | defer close(respCh) 64 | req := []interface{}{sessionID} 65 | cmd := command{commandLoadStore, req, respCh} 66 | m.commandCh <- cmd 67 | resp := <-respCh 68 | var res Store 69 | if resp.err != nil { 70 | e.Logger.Debugf("Session[%s] Load store Error. [%s]", sessionID, resp.err) 71 | return res, resp.err 72 | } 73 | if res, ok := resp.result[0].(Store); ok { 74 | return res, nil 75 | } 76 | e.Logger.Debugf("Session[%s] Load store Error. [%s]", sessionID, ErrorOther) 77 | return res, ErrorOther 78 | } 79 | 80 | // SaveStore は データストアの保存を行います。 81 | func (m *Manager) SaveStore(sessionID ID, sessionStore Store) error { 82 | respCh := make(chan response, 1) 83 | defer close(respCh) 84 | req := []interface{}{sessionID, sessionStore} 85 | cmd := command{commandSaveStore, req, respCh} 86 | m.commandCh <- cmd 87 | resp := <-respCh 88 | if resp.err != nil { 89 | e.Logger.Debugf("Session[%s] Save store Error. [%s]", sessionID, resp.err) 90 | return resp.err 91 | } 92 | return nil 93 | } 94 | 95 | // Delete は セッションの削除を行います。 96 | func (m *Manager) Delete(sessionID ID) error { 97 | respCh := make(chan response, 1) 98 | defer close(respCh) 99 | req := []interface{}{sessionID} 100 | cmd := command{commandDelete, req, respCh} 101 | m.commandCh <- cmd 102 | resp := <-respCh 103 | if resp.err != nil { 104 | e.Logger.Debugf("Session[%s] Delete Error. [%s]", sessionID, resp.err) 105 | return resp.err 106 | } 107 | return nil 108 | } 109 | 110 | // DeleteExpired は 期限切れセッションの削除を行います。 111 | func (m *Manager) DeleteExpired() error { 112 | respCh := make(chan response, 1) 113 | defer close(respCh) 114 | cmd := command{commandDelete, nil, respCh} 115 | m.commandCh <- cmd 116 | resp := <-respCh 117 | if resp.err != nil { 118 | e.Logger.Debugf("Session DeleteExpired Error. [%s]", resp.err) 119 | return resp.err 120 | } 121 | return nil 122 | } 123 | 124 | // Managerが返す各エラーのインスタンスを生成します。 125 | var ( 126 | ErrorBadParameter = errors.New("Bad Parameter") 127 | ErrorNotFound = errors.New("Not Found") 128 | ErrorInvalidToken = errors.New("Invalid Token") 129 | ErrorInvalidCommand = errors.New("Invalid Command") 130 | ErrorNotImplemented = errors.New("Not Implemented") 131 | ErrorOther = errors.New("Other") 132 | ) 133 | -------------------------------------------------------------------------------- /webserver/session/manager_local.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/labstack/echo" 7 | uuid "github.com/satori/go.uuid" 8 | ) 9 | 10 | // echoのインスタンス 11 | var e *echo.Echo 12 | 13 | // セッション毎の情報 14 | type session struct { 15 | store Store 16 | expire time.Time 17 | } 18 | 19 | // セッションの有効期限 20 | const sessionExpire time.Duration = (3 * time.Minute) 21 | 22 | // コマンド種別の定義 23 | type commandType int 24 | 25 | const ( 26 | commandCreate commandType = iota // セッションの作成 27 | commandLoadStore // データストアの読み出し 28 | commandSaveStore // データストアの保存 29 | commandDelete // セッションの削除 30 | commandDeleteExpired // 期限切れのセッションを削除 31 | ) 32 | 33 | // コマンド実行のためのパラメータ 34 | type command struct { 35 | cmdType commandType 36 | req []interface{} 37 | responseCh chan response 38 | } 39 | 40 | // コマンド実行の結果 41 | type response struct { 42 | result []interface{} 43 | err error 44 | } 45 | 46 | // Manager のメインループ処理 47 | func (m *Manager) mainLoop() { 48 | sessions := make(map[ID]session) 49 | m.stopCh = make(chan struct{}, 1) 50 | m.commandCh = make(chan command, 1) 51 | defer close(m.commandCh) 52 | defer close(m.stopCh) 53 | e.Logger.Info("session.Manager:start") 54 | loop: 55 | for { 56 | // 受信したコマンドによって処理を振り分ける 57 | select { 58 | case cmd := <-m.commandCh: 59 | switch cmd.cmdType { 60 | // セッションの作成 61 | case commandCreate: 62 | sessionID := ID(createSessionID()) 63 | session := session{} 64 | sessionStore := Store{} 65 | sessionData := make(map[string]string) 66 | sessionStore.Data = sessionData 67 | sessionStore.ConsistencyToken = createToken() 68 | session.store = sessionStore 69 | session.expire = time.Now().Add(sessionExpire) 70 | sessions[sessionID] = session 71 | res := []interface{}{sessionID} 72 | e.Logger.Debugf("Session[%s] Create. expire[%s]", sessionID, session.expire) 73 | cmd.responseCh <- response{res, nil} 74 | // データストアの読み出し 75 | case commandLoadStore: 76 | reqSessionID, ok := cmd.req[0].(ID) 77 | if !ok { 78 | cmd.responseCh <- response{nil, ErrorBadParameter} 79 | break 80 | } 81 | session, ok := sessions[reqSessionID] 82 | if !ok { 83 | cmd.responseCh <- response{nil, ErrorNotFound} 84 | break 85 | } 86 | if time.Now().After(session.expire) { 87 | cmd.responseCh <- response{nil, ErrorNotFound} 88 | break 89 | } 90 | sessionStore := Store{} 91 | sessionData := make(map[string]string) 92 | for k, v := range session.store.Data { 93 | sessionData[k] = v 94 | } 95 | sessionStore.Data = sessionData 96 | sessionStore.ConsistencyToken = session.store.ConsistencyToken 97 | session.expire = time.Now().Add(sessionExpire) 98 | sessions[reqSessionID] = session 99 | e.Logger.Debugf("Session[%s] Load store. store[%s] expire[%s]", reqSessionID, session.store, session.expire) 100 | res := []interface{}{sessionStore} 101 | cmd.responseCh <- response{res, nil} 102 | // データストアの保存 103 | case commandSaveStore: 104 | reqSessionID, ok := cmd.req[0].(ID) 105 | if !ok { 106 | cmd.responseCh <- response{nil, ErrorBadParameter} 107 | break 108 | } 109 | reqSessionStore, ok := cmd.req[1].(Store) 110 | if !ok { 111 | cmd.responseCh <- response{nil, ErrorBadParameter} 112 | break 113 | } 114 | session, ok := sessions[reqSessionID] 115 | if !ok { 116 | cmd.responseCh <- response{nil, ErrorNotFound} 117 | break 118 | } 119 | if time.Now().After(session.expire) { 120 | cmd.responseCh <- response{nil, ErrorNotFound} 121 | break 122 | } 123 | if session.store.ConsistencyToken != reqSessionStore.ConsistencyToken { 124 | cmd.responseCh <- response{nil, ErrorInvalidToken} 125 | break 126 | } 127 | sessionStore := Store{} 128 | sessionData := make(map[string]string) 129 | for k, v := range reqSessionStore.Data { 130 | sessionData[k] = v 131 | } 132 | sessionStore.Data = sessionData 133 | sessionStore.ConsistencyToken = createToken() 134 | session.store = sessionStore 135 | session.expire = time.Now().Add(sessionExpire) 136 | sessions[reqSessionID] = session 137 | e.Logger.Debugf("Session[%s] Save store. store[%s] expire[%s]", reqSessionID, session.store, session.expire) 138 | cmd.responseCh <- response{nil, nil} 139 | // セッションの削除 140 | case commandDelete: 141 | reqSessionID, ok := cmd.req[0].(ID) 142 | if !ok { 143 | cmd.responseCh <- response{nil, ErrorBadParameter} 144 | break 145 | } 146 | session, ok := sessions[reqSessionID] 147 | if !ok { 148 | cmd.responseCh <- response{nil, ErrorNotFound} 149 | break 150 | } 151 | if time.Now().After(session.expire) { 152 | cmd.responseCh <- response{nil, ErrorNotFound} 153 | break 154 | } 155 | delete(sessions, reqSessionID) 156 | e.Logger.Debugf("Session[%s] Delete.", reqSessionID) 157 | cmd.responseCh <- response{nil, nil} 158 | // 期限切れのセッションを削除 159 | case commandDeleteExpired: 160 | e.Logger.Debugf("Run Session GC. Now[%s]", time.Now()) 161 | for k, v := range sessions { 162 | if time.Now().After(v.expire) { 163 | e.Logger.Debugf("Session[%s] expire delete. expire[%s]", k, v.expire) 164 | delete(sessions, k) 165 | } 166 | } 167 | cmd.responseCh <- response{nil, nil} 168 | // それ以外(エラー) 169 | default: 170 | cmd.responseCh <- response{nil, ErrorInvalidCommand} 171 | } 172 | case <-m.stopCh: 173 | break loop 174 | } 175 | } 176 | e.Logger.Info("session.Manager:stop") 177 | } 178 | 179 | // 期限切れセッションの定期削除処理 180 | func (m *Manager) gcLoop() { 181 | m.stopGCCh = make(chan struct{}, 1) 182 | defer close(m.stopGCCh) 183 | e.Logger.Info("session.Manager GC:start") 184 | t := time.NewTicker(1 * time.Minute) 185 | loop: 186 | for { 187 | select { 188 | case <-t.C: 189 | respCh := make(chan response, 1) 190 | defer close(respCh) 191 | cmd := command{commandDeleteExpired, nil, respCh} 192 | m.commandCh <- cmd 193 | <-respCh 194 | case <-m.stopGCCh: 195 | break loop 196 | } 197 | } 198 | t.Stop() 199 | e.Logger.Info("session.Manager GC:stop") 200 | } 201 | 202 | // 新規セッションIDの発行 203 | func createSessionID() string { 204 | return uuid.NewV4().String() 205 | } 206 | 207 | // 新規整合性トークンの発行 208 | func createToken() string { 209 | return uuid.NewV4().String() 210 | } 211 | -------------------------------------------------------------------------------- /webserver/setting/setting.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Server はサーバーの動作に関する設定です。 8 | var Server = server{} 9 | 10 | type server struct { 11 | Port string 12 | } 13 | 14 | // Session はセッションに関する設定です。 15 | var Session = session{} 16 | 17 | type session struct { 18 | CookieName string 19 | CookieExpire time.Duration 20 | } 21 | 22 | // Load は設定を読み込みます。 23 | func Load() { 24 | // ポート番号 25 | Server.Port = ":3000" 26 | // セッションのCookie名 27 | Session.CookieName = "gowebserver_session_id" 28 | // セッションのCookie有効期限 29 | Session.CookieExpire = (1 * time.Hour) 30 | } 31 | -------------------------------------------------------------------------------- /webserver/static.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/labstack/echo" 5 | ) 6 | 7 | // 静的ファイルを配置するルーティングを設定 8 | func setStaticRoute(e *echo.Echo) { 9 | e.Static("/public/css/", "./public/css") 10 | e.Static("/public/js/", "./public/js/") 11 | e.Static("/public/img/", "./public/img/") 12 | } 13 | -------------------------------------------------------------------------------- /webserver/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "io" 6 | 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | // Template はHTMLテンプレートを利用するためのRenderer Interfaceです。 11 | type Template struct { 12 | } 13 | 14 | // Render はHTMLテンプレートにデータを埋め込んだ結果をWriterに書き込みます。 15 | func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 16 | if t, ok := templates[name]; ok { 17 | return t.ExecuteTemplate(w, "layout.html", data) 18 | } 19 | c.Echo().Logger.Debugf("Template[%s] Not Found.", name) 20 | return templates["error"].ExecuteTemplate(w, "layout.html", "Internal Server Error") 21 | } 22 | 23 | // HTMLテンプレートの読み込み 24 | func loadTemplates() { 25 | var baseTemplate = "templates/layout.html" 26 | templates = make(map[string]*template.Template) 27 | // 各HTMLテンプレートに共通レイアウトを適用した結果をmapに保存する 28 | templates["index"] = template.Must( 29 | template.ParseFiles(baseTemplate, "templates/index.html")) 30 | templates["error"] = template.Must( 31 | template.ParseFiles(baseTemplate, "templates/error.html")) 32 | templates["user"] = template.Must( 33 | template.ParseFiles(baseTemplate, "templates/user.html")) 34 | templates["login"] = template.Must( 35 | template.ParseFiles(baseTemplate, "templates/login.html")) 36 | templates["admin"] = template.Must( 37 | template.ParseFiles(baseTemplate, "templates/admin.html")) 38 | templates["admin_users"] = template.Must( 39 | template.ParseFiles(baseTemplate, "templates/admin_users.html")) 40 | } 41 | -------------------------------------------------------------------------------- /webserver/templates/admin.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |

Administrator Page

3 |
4 |
5 | 6 |
7 |
8 |
9 | 10 |
11 | {{end}} -------------------------------------------------------------------------------- /webserver/templates/admin_users.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |

ユーザー一覧

3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{range .}} 14 | 15 | 16 | 17 | 18 | 19 | {{end}} 20 | 21 |
User IDFull NameRole
{{.UserID}}{{.FullName}}{{.Roles}}
22 |
23 | 24 |
25 | {{end}} -------------------------------------------------------------------------------- /webserver/templates/error.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |

Error

3 |

{{.}}

4 | {{end}} -------------------------------------------------------------------------------- /webserver/templates/index.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |

Hello {{.}}!

3 | {{end}} 4 | -------------------------------------------------------------------------------- /webserver/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Go Website Sample 4 | 6 | 7 | 8 |
9 |
10 |
11 | 12 | {{template "content" .}} 13 |
14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /webserver/templates/login.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |

Login

3 |
4 |

5 | 6 | 7 |

8 |

9 | 10 | 11 |

12 | 13 |
14 |

15 | {{.msg}} 16 |

17 | {{end}} 18 | -------------------------------------------------------------------------------- /webserver/templates/user.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |

User Detail

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
User ID{{.UserID}}
Full Name{{.FullName}}
11 |
12 | 13 |
14 | {{end}} 15 | --------------------------------------------------------------------------------