Implementing Authentication Logic
เนื่องจากเรามี 3 ส่วนที่ต้อง implement เดี่ยวจะเรื่มจากชั้นที่อยู่นอกที่สุดก่อนเดี๋ยวจะเรื่มจากชั้นที่อยู่นอกสุดนะครับ
ก่อนจะมา handle function ต่างๆส่วนมากผมจะทำต่าง ๆ ส่วนมากผมจะทำ function utils ต่างๆเพื่อมาช่วยในเราการต่าง ๆ เพื่อมาช่วยในการ dev ก่อนและก่อน และ 1 function สำคัญที่ขาดไม่ได้เลยก็คือสำคัญที่ขาดไม่ได้เลย ก็คือ bodyparser และเราจะไม่ไปลงและเราจะไม่ลง lib ที่ไหนครับเพราะผมลองบางตัวแล้วไม่ถูกใจเนื่องจากผมอยากจะให้ตัวที่ไหนครับ เพราะผมลองบางตัวแล้วไม่ถูกใจเนื่องจากผมอยากให้ตัว body นั้นถูกยัดเข้าไปใน type ของเราเลยนั้นเองของเราเลยนั่นเอง งั้นเรามาเขียนเองเลยดีกว่าครับงั้นเรามาเขียนเองกันเลยดีกว่าครับ
ส่วนคนที่ไม่ทราบว่า bodyparser คืออะไร มาอ่านส่วนนี้กันก่อนครับเรามาอ่านส่วนนี้กันก่อนครับ
bodyparser คือ function ที่จะแปลง body ของ request ที่ clientClient เรียกมาออกมาเป็น format ต่างๆไม่ว่าจะเป็นต่าง ๆ ไม่ว่าจะเป็น json หรืออื่นๆหรืออื่น ๆ
สร้างไฟล์ bodyparser.go ขี้นมาแล้วใส่โค้ดนี้ลงไปครับขี้นมาแล้วใส่โค้ดนี้ลงไปได้เลยครับ
package utils
func Parse[T any](r *http.Request, body T) error {
b, err := io.ReadAll(r.Body)
if err != nil {
return err
}
if err := json.Unmarshal([]byte(b), &body); err != nil {
return err
}
return nil
}
function ที่เราจะทำนั้นผมทำให้เป็น generic เพื่อที่จะได้สามารถรับเพื่อสามารถรับ type อะไรก็ได้แล้วเอาอะไรก็ได้ แล้วเอา value มาใส่ให้ลงไปในใส่ลงไปใน poiner ของตัวแปล อย่างที่เห็นคือผมรับซึ่งอย่างที่เห็นคือผมรับ request (สิ่งที่เก็บ value ของ body) ของ net/http และตัวแปลของเราซึ่งเป็น generic type
ต่อมาเราจะอ่าน body ออกมาเป็นให้ออกมาเป็น byte array หรือ ในที่นี้คือหรือในที่นี้คือ json ที่ถูก encode อยู่ และนำมา unmarshal เข้า pointer ของตัวแปล และหากไม่มี error ก็ให้ return ค่า nil กลับไป เท่านี้ก็จะได้เท่านี้เราก็จะได้ bodyparser ง่ายๆมาแล้วครับง่าย ๆ มาแล้วครับ
SignUp (POST request)
Overall Spec
หลังจากเห็นภาพรวม service เราไปแล้วก่อนจะทำเราไปแล้ว ก่อนจะทำ function POST ใดๆผมจะทำใด ๆ ผมจะสร้าง struct ของ request ไว้ก่อนครับเพื่อให้ง่ายต่อการรับไว้ก่อนครับ เพื่อให้ง่ายต่อการรับ body เพราะเหตุผลที่เราสร้างเพราะสาเหตุที่เราสร้าง function bodyparser เพื่อให้เราสามารถสร้างคือให้เราสามารถสร้าง type ไว้รับ body ได้โดยง่ายนั้นเองได้โดยง่ายนั่นเอง
สร้าง folder type และสร้างไฟล์ user.go ด้านใน
package types
type SignIn struct {
Email string `json:"email"`
Password string `json:"password"`
}
การสร้าง struct เราควรจะใส่ชื่อ attribute เป็นรูปแบบ Capitalize เพราะ golangGolang จะมองว่า attribute นั้นเป็น private และไม่สามารถดึงมาใช้ได้นั้นเองและไม่สามารถดึงมาใช้ได้นั่นเอง ส่วนด้านหลังเอาไว้ unmarshal value เข้าไปนั้นเองดังน้ันเราควรจะตั้งให้เหมือนกับเข้าไป ดังน้ันเราควรจะตั้งให้เหมือนกับ request ที่จะได้รับ
Handler
func (h userHandler) SignUp(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
var response []byte
var body types.SignIn
err := utils.Parse(r, &body)
if err != nil {
response_value := map[string]any{"success": false, "error": err.Error()}
response, _ := json.Marshal(response_value)
w.Write(response)
return
}
token, base64, err := h.service.SignUp(body.Email, body.Password)
if err != nil {
response_value := map[string]any{"success": false, "error": err.Error()}
response, _ := json.Marshal(response_value)
w.Write(response)
return
}
response, _ = json.Marshal(map[string]any{"success": true, "token": token, "image": base64})
w.Write(response)
return
}
ก่อนอื่นใน function นี้ผมตรวจนี้ผมจะตรวจ method ของ request นี้ก่อนและค่อย set content-type เป็น application/json ที่เราคุ้นหน้าคุ้นตาเพื่อให้ที่เราคุ้นเคย เพื่อให้ content ออกมาเป็นรูปแบบที่เราต้องการจริงๆออกมาเป็นรูปแบบที่เราต้องการจริง ๆ ต่อมา parse body โดยใช้ function ที่เราสร้างก่อนหน้านี้ และหากเกิด error ผมได้ผมมีการ handle ไว้และส่ง response ที่เราต้องการกลับไป และ return ด้วยเพราะไม่งั้นจะเกิดด้วย เพราะไม่งั้นจะเกิด error เนื่องจากเรา write ไปแล้ว แล้วดันมีคำสั่งแต่ดันมีคำสั่ง write ใหม่
ลำดับต่อมาก็จะเป็นขั้นตอนการเรียก function ที่จะไปทำที่จะทำ business logic ของเรา และรับค่าที่ return ออกมาซึ่งในออกมา ซึ่งใน spec ของเราคือเราจะส่งของเราคือการส่ง token และรูปที่เป็น base64 กลับมานั้นเองกลับมานั่นเอง หลังจากทำ function สำเร็จและไม่มีข้อผิดพลาดเราก็จะปั้นตัวสำเร็จและไม่มีข้อผิดพลาด เราก็จะปั้นตัว response ของเราและ write กล้บไปให้ user นั้นเองนั่นเอง
Service
ก่อนอื่น ลงเริ่มจากการลง library 2 ตัวก่อนซึ่งสำคัญกับการทำระบบตัวก่อน authentication ของเรานั้นคือนั่นคือ TOTP, และ JWT ที่ซึ่งสำคัญกับการทำระบบ authentication
$ go get github.com/pquerna/otp/totp
$ go get github.com/golang-jwt/jwt
func (s userService) SignUp(email string, password string) (*string, *string, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "GDSC KMUTT",
AccountName: email,
})
if err != nil {
return nil, nil, err
}
secret := key.Secret()
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, nil, err
}
user, err := s.repository.CreateUser(email, string(hashedPwd), secret)
if err != nil {
return nil, nil, err
}
claims := jwt.MapClaims{
"id": user.Id,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(config.C.JWT_SECRET))
// Convert TOTP key into a PNG
var buf bytes.Buffer
img, err := key.Image(200, 200)
if err != nil {
return nil, nil, err
}
if err := png.Encode(&buf, img); err != nil {
return nil, nil, err
}
base64string := "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
return &tokenString, &base64string, nil
}
ส่วน service จะเยอะนิดนึงครับ เพราะเป็นส่วนที่รวม business logic ทั้งหมดที่จะประมวณผลข้อมูลที่เราได้มาออกมาเป็นสิ่งที่เราต้องการทั้งหมดที่จะประมวณผลข้อมูลที่เรารับออกมาเป็นสิ่งที่เราต้องการ
เริ่มจากพระเอกของเรา TOTP ที่เราจะมาลอง implement กัน ซึ่งหลักการของมันไม่ได้มีอะไรซับซ้อนครับ ก็คือเราจะซึ่งหลักการของมันก็คือเราจะ generate key ของ user คนปัจจุบันมาก่อน และเราก็จะได้ object key ซึ่งมีข้อมูลสำคัญที่เราต้องเก็บนั้นคือซึ่งมีข้อมูลสำคัญที่เราต้องเก็บ นั่นคือ secret ของ user นั้นเอง และ object key นี้สามารถดีงค่าอย่างอื่นออกมาได้ด้วย เช่น QRcode ที่เอาไว้สแกนโดยไม่ต้องใส่ secret โดยตรง
ดังนั้นใน function create user ผมเลยใส่ parameter secret เข้าไปด้วยเพื่อที่จะเอาเข้าไปด้วย เพื่อที่จะเอา secret ไปเก็บด้วยนั้นเองไปเก็บด้วยนั่นเอง คราวนี้หลังได้หลังจากได้ key มาเราก็มาทำให้มา เราก็จะมาทำให้ password ของ user ปลอดภัยขึ้นด้วยการปลอดภัยขึ้น ด้วยการ hashed นั้นเอง ซึ่งจะทำให้ password กลายเป็น text ที่เราอ่านไม่ออก เพื่อที่จะให้คนที่ดูแลที่เราอ่านไม่ออกและทำให้คนที่ดูแล database ไม่สามารถดู password ของ user ได้นั้นเองได้ และหลังจากได้ข้อมูลทั้งหมดที่จะนำไปเก็บในหลังจากรับข้อมูลทั้งหมดที่จะนำไปเก็บใน database เราก็เรียกแล้วเราก็เรียก function create user ผ่าน "Port"Port ที่เราสร้างได้เลย
หลังจากที่ได้ user ที่ return กลับมาเราก็นำมาสร้าง token JWT ได้เลยโดยกำหนดสื่งที่จะอยู่ในได้เลย โดยกำหนดสิ่งที่จะอยู่ใน token (ควรมี exp ไว้ตลอด เพราะเป็นอายุการใช้งานของเพื่อบ่งบอกอายุการใช้งานของ token) ในที่นี้เราใส่แค่ id ของ user ไปก่อน หลังจากได้ token ก็นำไปเข้า function signedstring ซึ่งจุดนี้เราจำเป็นต้องใช้ secret ของเราที่เราได้ที่เราได้ set ไปแล้วใน config ดังนั้นเราก็เลยเรียกได้เลยเช่นเดียวกับตอนใส่ดังนั้นเราจึงเรียกได้เลย เช่นเดียวกับตอนใส่ connection string ของ database
ต่อมาเพื่อที่จะได้รูปมาเป็น format base64 เราไม่สามาถทำได้ทันทีเนื่องจาก library TOTP ไม่ได้ทำ method มาให้เราจึงต้องมาแปลงจากมาให้ เราจึงต้องมาแปลงจาก img ที่เป็น type Image.image เอาเองเอง และวิธีคือเรานำซึ่งวิธีคือการนำ img เข้าไปใน function encode โดยที่จะเอา data เข้าไปเก็บใน bytes.buffer ทีนี้เราก็แค่นำและนำ img ที่เป็น byte.buffer ไปแปลงเป็น base64 โดยใช้ function base64 encode ได้เลย (ที่ต้องจำเป็นต้อง concat string เข้าไปด้านหน้าก่อนเพราะจำเป็นต้องบอกก่อนว่าเข้าไปด้านหน้าก่อนเพราะต้องบอกก่อนว่า type ของ data เป็นของข้อมูลเป็น image นั้นเอง)นั่นเอง) เท่านี้เราก็ return ค่า token กับ base64 กลับไปได้เลย
Repository
func (u userRepositoryDB) CreateUser(email string, password string, secret string) (*User, error) {
insert, err := u.db.Exec("INSERT INTO users (email, password, secret) VALUES (?, ?, ?)", email, password, secret)
if err != nil {
return nil, err
}
userId, err := insert.LastInsertId()
var user = User{
Id: userId,
Email: email,
Password: password,
Secret: secret,
}
return &user, nil
}
ส่วนนี้จะทำหน้าที่แค่อย่างเดียวคือการส่วนนี้จะทำหน้าที่แค่อย่างเดียว คือการ insert data เข้าข้อมูลเข้า database ดังนั้นจุดที่สำคัญคือการเขียน query สำหรับ insert นั้นเองนั่นเอง และเนื่องจากใน spec เราเขียนไว้ว่าจะเอา user data ไปใช้ด้วยดังนั้นเราอย่าลืมปั่นไปใช้ด้วย ดังนั้นเราอย่าลืมสร้าง model user ตามที่สร้างไว้ใน repositoryRepository แล้วส่ง return กลับไปด้วย
เท่านี้การทำ serviceService สำหรับ signup หรือ register ก็เสร็จสมบูรณ์แล้วแต่ยังขาดอีกก็เสร็จสมบูรณ์แล้ว แต่ยังขาดอีก service ที่สำคัญสำหรับการทำระบบ authentication ซึ่งมันก็คือระบบซึ่งก็คือระบบ signin
นอกจากนี้เรามีเซอร์ไพรส์ครับ และเรามี surprise ครับเพราะเพราะ GDSC session ครั้งนี้จะให้ผู้ชมได้ลองทำเองด้วยในครั้งนี้จะให้ผู้ชมได้ลองทำเองด้วย และจะมีของรางวัลให้สำหรับผู้ที่และยังมีของรางวัลให้สำหรับผู้ที่ implement ได้ถูกต้อง 5 คนแรกด้วยคนแรก แต่ขอสงวนสิทธิ์ให้คนที่เข้าแต่ขอสงวนสิทธิ์ให้กับคนที่เข้า session onsite เท่านั้นนะครับแน่นอนว่าเรามีเท่านั้นนะครับ แน่นอนว่าเรามี documentation ให้ด้วยพร้อมทั้งถาม mentor ในให้ด้วยพร้อมทั้งสามารถถามเมนเทอร์ใน session ครั้งนี้ได้เต็มที่เลยครับขอให้โชคดีในครั้งนี้ได้เต็มที่เลยครับ ขอให้โชคดีใน session หลังบ่ายนี้ที่จะเป็นการทำให้ project นี้น่าสนใจขึ้นไปอีก และถือเป็นการ meetup สำหรับผู้ที่สนใจการเขียน code เพื่อพัฒนาสำหรับผู้ที่สนใจการเขียนโค้ดเพื่อพัฒนา community coder ในมหาลัยนี้ไปด้วยครับ
TOTP: https://github.com/pquerna/otp
