Google Oauth 관련 참고 자료
[https://unsungit.tistory.com/98?category=1060987]
[https://unsungit.tistory.com/99?category=1060987]
session 관련 패키지
go get github.com/gorilla/sessions
환경변수에 "SESSION_KEY" 추가할것. 키는 임의의 값으로 입력하면 됨.
이 키값으로 session 정보를 암호화하는 키로 사용된며, 차후에 변경가능.
- 기본구조
- "/" 로 접속하면 session 정보가 없으므로 singin.html 로 이동
- 이후 부터는 "http://localhost:3000/todo.html" 페이지로 자동 이동한다.
특이사항) 로그인시 "/auth/google/login" 접속할때 session 정보가 없지만 singin.html 이동하지 않게 처리 필요.
로그인 화면
main.go
package main
import (
"GO/tuckersGo/goWeb/web21-todo_login/myapp"
"log"
"net/http"
)
const portNumber = ":3000"
func main() {
mux := myapp.MakeNewHandler("./todo.db")
defer mux.Close()
// ng := negroni.Classic()
// ng.UseHandler(mux)
log.Println("Started App")
err := http.ListenAndServe(portNumber, mux)
if err != nil {
panic(err)
}
}
./myapp/app.go
package myapp
import (
"GO/tuckersGo/goWeb/web21-todo_login/model"
"log"
"net/http"
"os"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/unrolled/render"
"github.com/urfave/negroni"
)
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
var rd *render.Render = render.New()
type AppHandler struct {
http.Handler
db model.DBHandler
}
// session 정보를 가져오는 함수
func getSessionID(r *http.Request) string {
session, err := store.Get(r, "session")
if err != nil {
log.Println(err.Error())
return ""
}
// Set some session values.
val := session.Values["id"]
if val == nil {
return ""
}
return val.(string)
}
func (a *AppHandler) indexHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/todo.html", http.StatusTemporaryRedirect)
}
func (a *AppHandler) getTodoListHandler(w http.ResponseWriter, r *http.Request) {
list := a.db.GetTodos()
rd.JSON(w, http.StatusOK, list)
}
func (a *AppHandler) addTodoHandler(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
todo := a.db.AddTodo(name)
rd.JSON(w, http.StatusCreated, todo)
}
type Success struct {
Success bool `json:"success"`
}
func (a *AppHandler) removeTodoHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
ok := a.db.RemoveTodo(id)
if ok {
rd.JSON(w, http.StatusOK, Success{Success: true})
} else {
rd.JSON(w, http.StatusOK, Success{Success: false})
}
}
func (a *AppHandler) completeTodoHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
complete := r.FormValue("complete") == "true"
ok := a.db.CompleteTodo(id, complete)
if ok {
rd.JSON(w, http.StatusOK, Success{Success: true})
} else {
rd.JSON(w, http.StatusOK, Success{Success: false})
}
}
func (a *AppHandler) Close() {
a.db.Close()
}
// session 확인
func CheckSignin(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// if request URL is /singin.html, then next()
if strings.Contains(r.URL.Path, "/signin.html") || strings.Contains(r.URL.Path, "/auth") {
next(rw, r)
return
}
// if user already signed in
sessionID := getSessionID(r)
if sessionID != "" {
next(rw, r)
return
}
// if not user sign in
// redirect signin.html
http.Redirect(rw, r, "/signin.html", http.StatusTemporaryRedirect)
}
// 리턴변경 http.Handler -> AppHandler
func MakeNewHandler(filepath string) *AppHandler {
mux := mux.NewRouter()
// Classic() *Negroni => return New(NewRecovery(), NewLogger(), NewStatic(http.Dir("public")))
// ng := negroni.Classic()
ng := negroni.New(negroni.NewRecovery(), negroni.NewLogger(), negroni.HandlerFunc(CheckSignin), negroni.NewStatic(http.Dir("public")))
ng.UseHandler(mux)
a := &AppHandler{
Handler: ng, // mux->ng
db: model.NewDBHandler(filepath),
}
mux.HandleFunc("/", a.indexHandler)
mux.HandleFunc("/todos", a.getTodoListHandler).Methods("GET")
mux.HandleFunc("/todos", a.addTodoHandler).Methods("POST")
mux.HandleFunc("/todos/{id:[0-9]+}", a.removeTodoHandler).Methods("DELETE")
mux.HandleFunc("/complete-todo/{id:[0-9]+}", a.completeTodoHandler).Methods("GET")
mux.HandleFunc("/auth/google/login", googleLoginHandler)
mux.HandleFunc("/auth/google/callback", googleAuthCallback)
return a
}
./myapp/signin.go
package myapp
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
type GoogleUserId struct {
ID string `json:"id"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
Picture string `json:"picture"`
}
var googleOauthConfig = oauth2.Config{
RedirectURL: "http://localhost:3000/auth/google/callback",
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_SECRET_KEY"),
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: google.Endpoint,
}
func googleLoginHandler(w http.ResponseWriter, r *http.Request) {
state := generateStateOauthCookie(w)
url := googleOauthConfig.AuthCodeURL(state)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
// cookie 에 일회용 비번을 저장해서 검증
func generateStateOauthCookie(w http.ResponseWriter) string {
expiration := time.Now().Add(1 * 24 * time.Hour)
b := make([]byte, 16)
rand.Read(b)
state := base64.URLEncoding.EncodeToString(b)
cookie := &http.Cookie{Name: "oauthstate", Value: state, Expires: expiration}
http.SetCookie(w, cookie)
return state
}
func googleAuthCallback(w http.ResponseWriter, r *http.Request) {
oauthstate, _ := r.Cookie("oauthstate")
// 쿠키값과 폼데이터의 state 가 같은지 비교
if r.FormValue("state") != oauthstate.Value {
errMsg := fmt.Sprintf("invalid google oauth state cookie:%s state:%s\n", oauthstate.Value, r.FormValue("state"))
log.Println(errMsg)
http.Error(w, errMsg, http.StatusInternalServerError)
// http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
data, err := getGoogleUserInfo(r.FormValue("code"))
if err != nil {
log.Println(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
// http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
// store ID info into Session cookie
var userInfo GoogleUserId
err = json.Unmarshal(data, &userInfo)
if err != nil {
log.Println(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
// http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
session, err := store.Get(r, "session")
if err != nil {
log.Println(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set some session values.
session.Values["id"] = userInfo.ID
// Save it before we write to the response/return from the handler.
err = session.Save(r, w)
if err != nil {
log.Println(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
// fmt.Fprint(w, string(data))
}
const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="
func getGoogleUserInfo(code string) ([]byte, error) {
token, err := googleOauthConfig.Exchange(context.Background(), code)
if err != nil {
return nil, fmt.Errorf("Failed to Exchange %s\n", err.Error())
}
// RefreshToken 은 AccessToken 이 expired 된 경우, 다시 받을때 사용
resp, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
if err != nil {
return nil, fmt.Errorf("Failed to Get UserInfo %s\n", err.Error())
}
return ioutil.ReadAll(resp.Body)
}
./public/todo.html
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css">
<link rel="stylesheet" href="todo.css" >
<title>Hello, world!</title>
</head>
<body>
<div class="page-content page-container" id="page-content">
<div class="padding">
<div class="row container d-flex justify-content-center">
<div class="col-lg-12">
<div class="card px-3">
<div class="card-body">
<h4 class="card-title">Awesome Todo list</h4>
<div class="add-items d-flex"> <input type="text" class="form-control todo-list-input" placeholder="What do you need to do today?"> <button class="add btn btn-primary font-weight-bold todo-list-add-btn">Add</button> </div>
<div class="list-wrapper">
<ul class="d-flex flex-column-reverse todo-list">
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" ></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="todo.js"></script>
</body>
</html>
./public/signin.html
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css">
<link rel="stylesheet" href="signin.css" >
<title>Todos</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Sign In</h5>
<form class="form-signin">
<hr class="my-4">
<!-- form 에 걸려서 경로가 변경되지 않으므로 반드시 return false 해야 함 -->
<button onclick="window.location.href='/auth/google/login';return false;" class="btn btn-lg btn-google btn-block text-uppercase" type="submit"><i class="fab fa-google mr-2"></i> Sign in with Google</button>
<button class="btn btn-lg btn-facebook btn-block text-uppercase" type="submit"><i class="fab fa-facebook-f mr-2"></i> Sign in with Facebook</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
key.go - SESSION_KEY 자동 생성코드, 실행시마다 다른 키값이 출력된다.
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
id := uuid.New()
fmt.Println(id.String())
// 패키지 설치
go get github.com/google/uuid
PS D:\workspace\GO\tuckersGo\goWeb\web21-todo_login> go get github.com/google/uuid
go: downloading github.com/google/uuid v1.3.0
go get: added github.com/google/uuid v1.3.0
PS D:\workspace\GO\tuckersGo\goWeb\web21-todo_login>
//실행결과
PS D:\workspace\GO\tuckersGo\goWeb\web21-todo_login> go run key.go
242268c2-9749-48e8-bfec-edca30786497
PS D:\workspace\GO\tuckersGo\goWeb\web21-todo_login> go run key.go
88ae9db3-a7ec-4a8b-bd62-daf1c0e8a030
PS D:\workspace\GO\tuckersGo\goWeb\web21-todo_login>
참고자료 [유투브 링크]
'GO lang > web' 카테고리의 다른 글
[GO] Todo list - postgresql (0) | 2021.11.26 |
---|---|
[GO] Todo list - session 별 데이터 관리 (0) | 2021.11.23 |
[GO] Todo list - sqlite (0) | 2021.11.18 |
[GO] Todo list - interface 구현2 (0) | 2021.11.18 |
[GO] Todo list - interface 구현1 (0) | 2021.11.18 |