간단한 Chat App 구현하기 (3) - 인증 처리(로그인)

|

로그인 처리

로그인 화면 작성

/templates/login.tmpl

<html>
<head>
    <title>Login</title>
</head>
<body>
    <h3>먼저 로그인을 해주세요.</h3>
    <div>
        <ul>
            <li>
                <a href="/auth/login/google">Google</a>
            </li>
        </ul>
    </div>
</body>
</html>


main.go

package main

import (
	"github.com/unrolled/render"
	"github.com/julienschmidt/httprouter"
	"net/http"
	"github.com/goincremental/negroni-sessions/cookiestore"
	"github.com/goincremental/negroni-sessions"
	"github.com/urfave/negroni"
)

var renderer *render.Render

func init() {
	renderer = render.New()
}

const (
	sessionKey    = "simple-chat-app-session"
	sessionSecret = "simple-chat-app-session-secret"
)

func main() {
	router := httprouter.New()

	router.GET("/", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
		renderer.HTML(w, http.StatusOK, "index", map[string]string{"title": "Simple Chat App"})
	})

	router.GET("/login", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
		renderer.HTML(w, http.StatusOK, "login", nil)
	})

	n := negroni.Classic()

	store := cookiestore.New([]byte(sessionSecret))
	n.Use(sessions.Sessions(sessionKey, store))

	n.UseHandler(router)

	n.Run(":3000")
}

간단한 Chat App 구현하기 (2) - 인증 처리(세션 관리)

|

세션 관리

다음 패키지들을 이용해서 세션을 관리합니다.

각 패키지 설치는 다음 명령어를 이용해서 할 수 있습니다.

$ go get github.com/goincremental/negroni-sessions

$ go get github.com/stretchr/gomniauth

$ go get github.com/stretchr/gomniauth/providers/google


main.go

세션 관리를 위해 세션 핸들러를 등록합니다.

package main

import (
	"github.com/unrolled/render"
	"github.com/julienschmidt/httprouter"
	"net/http"
	"github.com/urfave/negroni"
	"github.com/goincremental/negroni-sessions/cookiestore"
	"github.com/goincremental/negroni-sessions"
)

var renderer *render.Render

func init() {
	renderer = render.New()
}

const (
	sessionKey    = "simple-chat-app-session"
	sessionSecret = "simple-chat-app-session-secret"
)

func main() {
	router := httprouter.New()

	router.GET("/", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
		renderer.HTML(w, http.StatusOK, "index", map[string]string{"title": "Simple Chat App"})
	})

	n := negroni.Classic()
	store := cookiestore.New([]byte(sessionSecret))
	n.Use(sessions.Sessions(sessionKey, store))

	n.UseHandler(router)

	n.Run(":3000")
}


session.go

package main

import (
	"time"
	"net/http"
	"github.com/goincremental/negroni-sessions"
	"encoding/json"
)

const (
	currentUserKey  = "oauth2_current_user"
	sessionDuration = time.Hour
)

type User struct {
	Uid       string    `json:"uid"`
	Name      string    `json:"name"`
	Email     string    `json:"user"`
	AvatarUrl string    `json:"avatar_url"`
	Expired   time.Time `json:"expired"'`
}

func (u *User) Valid() bool {
	return u.Expired.Sub(time.Now()) > 0
}

func (u *User) Refresh() {
	u.Expired = time.Now().Add(sessionDuration)
}
func GetCurrentUser(r *http.Request) *User {
	s := sessions.GetSession(r)

	if s.Get(currentUserKey) == nil {
		return nil
	}

	data := s.Get(currentUserKey).([]byte)

	var u User
	json.Unmarshal(data, &u)

	return &u
}

func SetCurrentUser(r *http.Request, u *User) {
	if u != nil {
		u.Refresh()
	}

	s := sessions.GetSession(r)
	val, _ := json.Marshal(u)
	s.Set(currentUserKey, val)
}

간단한 Chat App 구현하기 (1) - 간단한 웹 서버 구동

|

간단한 웹 서버 구동

다음 패키지들을 이용해서 간단한 웹 서버를 구동시키는 예제입니다.

각 패키지 설치는 다음 명령어를 이용해서 할 수 있습니다.

$ go get github.com/julienschmidt/httprouter

$ go get https://github.com/urfave/negroni


main.go

package main

import (
	"github.com/unrolled/render"
	"github.com/julienschmidt/httprouter"
	"net/http"
	"github.com/urfave/negroni"
)

var renderer *render.Render

func init() {
	renderer = render.New()
}

func main() {
	router := httprouter.New()

	router.GET("/", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
		renderer.HTML(w, http.StatusOK, "index", map[string]string{"title": "Simple Chat App"})
	})

	n := negroni.Classic()

	n.UseHandler(router)

	n.Run(":3000")
}


/templates/index.tmpl

<!DOCTYPE html>
<html lang="en">
<body>
    <h1></h1>
</body>
</html>

간단한 Web Framework 구현하기 (9) - Custom Middleware(Authorize)

|

Custom Middleware

/public/login.html

<html>
<head>
    <title>Login</title>
</head>
<body>
<form class="form-signin" action="/login" method="post">
    username
    <input name="username" required autofocus>
    password
    <input type="password" name="password" required>
    <button type="submit">Sign in</button>
</form>
</body>
</html>


middleware.go

package main

import (
	"time"
	"log"
	"net/http"
	"fmt"
	"encoding/json"
	"strings"
	"path"
	"crypto/hmac"
	"crypto/sha1"
	"io"
	"encoding/hex"
)

type Middleware func(next HandlerFunc) HandlerFunc

func logHandler(next HandlerFunc) HandlerFunc {
	return func(c *Context) {
		fmt.Println("logHandler is called.")

		t := time.Now()

		next(c)

		log.Printf("[%s] %q %v\n",
			c.Request.Method,
			c.Request.URL.String(),
			time.Now().Sub(t))
	}
}

func recoverHandler(next HandlerFunc) HandlerFunc {
	return func(c *Context) {
		fmt.Println("recoverHandler is called.")

		defer func() {
			if err := recover(); err != nil {
				log.Printf("panic: %+v", err)
				http.Error(c.ResponseWriter,
					http.StatusText(http.StatusInternalServerError),
					http.StatusInternalServerError)
			}
		}()
		next(c)
	}
}

func parseFormHandler(next HandlerFunc) HandlerFunc {
	return func(c *Context) {
		fmt.Println("HandlerFunc is called.")

		c.Request.ParseForm()
		fmt.Println(c.Request.PostForm)

		for k, v := range c.Request.PostForm {
			if len(v) > 0 {
				c.Params[k] = v[0]
			}
		}

		next(c)
	}
}

func parseJsonBodyHandler(next HandlerFunc) HandlerFunc {
	return func(c *Context) {
		fmt.Println("parseJsonBodyHandler is called.")

		var m map[string]interface{}

		if json.NewDecoder(c.Request.Body).Decode(&m); len(m) > 0 {
			for k, v := range m {
				c.Params[k] = v
			}
		}

		next(c)
	}
}

func staticHandler(next HandlerFunc) HandlerFunc {
	var (
		dir       = http.Dir(".")
		indexFile = "index.html"
	)

	return func(c *Context) {
		if c.Request.Method != "GET" && c.Request.Method != "HEAD" {
			next(c)
			return
		}

		file := c.Request.URL.Path
		f, err := dir.Open(file)
		if err != nil {
			next(c)
			return
		}
		defer f.Close()

		fi, err := f.Stat()
		if err != nil {
			next(c)
			return
		}

		if fi.IsDir() {
			if !strings.HasSuffix(c.Request.URL.Path, "/") {
				http.Redirect(c.ResponseWriter, c.Request, c.Request.URL.Path+"/", http.StatusFound)
				return
			}

			file = path.Join(file, indexFile)

			f, err = dir.Open(file)
			if err != nil {
				next(c)
				return
			}
			defer f.Close()

			fi, err = f.Stat()
			if err != nil || fi.IsDir() {
				next(c)
				return
			}
		}

		http.ServeContent(c.ResponseWriter, c.Request, file, fi.ModTime(), f)
	}
}

const VerifyMessage = "verified"

func AuthHandler(next HandlerFunc) HandlerFunc {
	ignore := []string{"/login", "public/index.html"}
	return func(c *Context) {
		for _, s := range ignore {
			if strings.HasPrefix(c.Request.URL.Path, s) {
				next(c)
				return
			}

			if v, err := c.Request.Cookie("X_AUTH"); err == http.ErrNoCookie {
				c.Redirect("/login")
			} else if err != nil {
				c.RenderError(http.StatusInternalServerError, err)
			} else if Verify(VerifyMessage, v.Value) {
				next(c)
				return
			}
		}

		c.Redirect("/login")
	}
}

func Verify(message, sig string) bool {
	return hmac.Equal([]byte(sig), []byte(Sign(message)))
}

func Sign(message string) string {
	secretKey := []byte("snowdeer-simple-web-framework")
	if len(secretKey) == 0 {
		return ""
	}

	mac := hmac.New(sha1.New, secretKey)
	io.WriteString(mac, message)

	return hex.EncodeToString(mac.Sum(nil))
}


main.go

package main

import (
	"fmt"
	"time"
	"net/http"
)

type User struct {
	Id   string
	Name string
}

func main() {
	fmt.Println("Simple Web Framework")

	s := NewServer()

	s.HandleFunc("GET", "/", func(c *Context) {
		c.RenderTemplate("/public/index.html", map[string]interface{}{"time": time.Now()})
	})

	s.HandleFunc("GET", "/about", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "This is an about page.")
	})

	s.HandleFunc("GET", "/users/:id", func(c *Context) {
		u := User{Id: c.Params["id"].(string)}

		c.RenderXml(u)
	})

	s.HandleFunc("GET", "/users/:id/name/:name", func(c *Context) {
		u := User{Id: c.Params["id"].(string),
			Name: c.Params["name"].(string),
		}

		c.RenderJson(u)
	})

	s.HandleFunc("POST", "/users", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "Create user")
	})

	s.HandleFunc("GET", "/login", func(c *Context) {
		c.RenderTemplate("/public/login.html", map[string]interface{}{"message": "로그인이 필요합니다."})
	})

	s.HandleFunc("POST", "/login", func(c *Context) {

		if CheckLogin(c.Params["username"].(string), c.Params["password"].(string)) {
			http.SetCookie(c.ResponseWriter, &http.Cookie{
				Name:  "X_AUTH",
				Value: Sign(VerifyMessage),
				Path:  "/",
			})
			c.Redirect("/")
		}
		c.RenderTemplate("/public/login.html", map[string]interface{}{"message": "id 또는 password 오류"})

	})

	s.Use(AuthHandler)
	s.Run(":8080")
}

func CheckLogin(username, password string) bool {
	const (
		USERNAME = "snowdeer"
		PASSWORD = "1234"
	)

	return (username == USERNAME) && (password == PASSWORD)
}

간단한 Web Framework 구현하기 (8) - Renderer (Template)

|

Renderer XML/JSON

/public/index.html

<html>
<body>
Hello SnowDeer
<p>
    current time: 
</p>
</body>
</html>


context.go

package main

import (
	"net/http"
	"encoding/json"
	"encoding/xml"
	"path/filepath"
	"html/template"
)

type Context struct {
	Params map[string]interface{}

	ResponseWriter http.ResponseWriter
	Request        *http.Request
}

type HandlerFunc func(*Context)

func (c *Context) RenderError(code int, err error) {
	if err != nil {
		errorCode := http.StatusInternalServerError

		if code > 0 {
			errorCode = code
		}

		http.Error(c.ResponseWriter, http.StatusText(errorCode), errorCode)
	}
}

func (c *Context) RenderJson(v interface{}) {
	c.ResponseWriter.WriteHeader(http.StatusOK)
	c.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")

	if err := json.NewEncoder(c.ResponseWriter).Encode(v); err != nil {
		c.RenderError(http.StatusInternalServerError, err)
	}
}

func (c *Context) RenderXml(v interface{}) {
	c.ResponseWriter.WriteHeader(http.StatusOK)
	c.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")

	if err := xml.NewEncoder(c.ResponseWriter).Encode(v); err != nil {
		c.RenderError(http.StatusInternalServerError, err)
	}
}

func (c *Context) Redirect(url string) {
	http.Redirect(c.ResponseWriter, c.Request, url, http.StatusMovedPermanently)
}

var templates = map[string]*template.Template{}

func (c *Context) RenderTemplate(path string, v interface{}) {
	t, ok := templates[path]
	if !ok {
		t = template.Must(template.ParseFiles(filepath.Join(".", path)))
		templates[path] = t
	}

	t.Execute(c.ResponseWriter, v)
}


main.go

package main

import (
	"fmt"
	"time"
)

type User struct {
	Id   string
	Name string
}

func main() {
	fmt.Println("Simple Web Framework")

	s := NewServer()

	s.HandleFunc("GET", "/", func(c *Context) {
		c.RenderTemplate("/public/index.html", map[string]interface{}{"time": time.Now()})
	})

	s.HandleFunc("GET", "/about", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "This is an about page.")
	})

	s.HandleFunc("GET", "/users/:id", func(c *Context) {
		u := User{Id: c.Params["id"].(string)}

		c.RenderXml(u)
	})

	s.HandleFunc("GET", "/users/:id/name/:name", func(c *Context) {
		u := User{Id: c.Params["id"].(string),
			Name: c.Params["name"].(string),
		}

		c.RenderJson(u)
	})

	s.HandleFunc("POST", "/users", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "Create user")
	})

	s.Run(":8080")
}