ผู้คนจัดการการพิสูจน์ตัวตนใน Go อย่างไร [ปิด]


187

สำหรับการสร้าง RESTful APIs และแอป Front-end ใน Go คุณจะจัดการการรับรองความถูกต้องอย่างไร คุณใช้ห้องสมุดหรือเทคนิคเฉพาะหรือไม่?

ฉันประหลาดใจที่พบการสนทนาเล็กน้อยเกี่ยวกับเรื่องนี้ ฉันจำคำตอบดังต่อไปนี้และพยายามหลีกเลี่ยงการพัฒนาการใช้งานของฉัน:

แบบฟอร์มการรับรองความถูกต้องใน ASP.Net

ทุกคนเขียนรหัสโซลูชันของตนเองแยกกันหรือไม่


5
การตรวจสอบความถูกต้องขึ้นอยู่กับประเภทของแอปพลิเคชั่นที่คุณใช้ ไม่มีวิธีแก้ปัญหาที่เหมาะกับทุกขนาด นอกจากนี้มันเป็นปัญหายากที่จะแก้ไข นี่เป็นโอกาสที่คุณจะไม่พบเอกสารข้อสรุปใด ๆ
jimt

21
เฮ้ขอบคุณสำหรับการตอบสนองที่รวดเร็ว เข้าใจ แต่ภาษาและกรอบงานส่วนใหญ่เกิดขึ้นกับโซลูชันการตรวจสอบความถูกต้องที่ครอบคลุมข้อกำหนดการตรวจสอบความถูกต้องที่ใช้กันทั่วไปโดยส่วนใหญ่ของแอพและมีการมีส่วนร่วมและการสนับสนุนจากชุมชนในวงกว้าง ฉันยอมรับว่ามันเป็นปัญหาที่ยาก ไม่ได้รับประโยชน์มากที่สุดจากการทำงานร่วมกันใช่หรือไม่ (นี่ไม่ใช่การร้องเรียนเพราะเป็นโอเพ่นซอร์ส แต่เป็นข้อสังเกตมากกว่าที่เราทุกคนกำลังคิดค้นวงล้อใหม่ :)
SexxLuthor

13
@ jimt ความจริงที่ว่ามันเป็นปัญหาที่ยากลำบากยิ่งทำให้มันสำคัญยิ่งกว่าที่จะให้มนุษย์ปุถุชนด้วยวิธีแก้ปัญหาแบบ Cononical ที่เราไม่สามารถจะผิดได้
tymtam

ฉันลงคะแนนให้ปิดคำถามนี้เป็นนอกหัวข้อเพราะเป็นคำถามแบบสำรวจความคิดเห็น
Flimzy

คำตอบ:


115

คำถามนี้ได้รับมุมมองมากมาย - และมีตราคำถามยอดนิยม - ดังนั้นฉันรู้ว่ามีความสนใจแฝงอยู่มากมายในหัวข้อนี้และหลายคนกำลังถามในสิ่งเดียวกันและไม่พบคำตอบในเว็บ interwebs

ข้อมูลส่วนใหญ่ที่มีอยู่ส่งผลให้ข้อความเป็นคลื่นของสิ่งที่เขียนด้วยมือซึ่งเหลือเป็น "แบบฝึกหัดสำหรับผู้อ่าน" ;)

อย่างไรก็ตามในที่สุดฉันก็ได้พบตัวอย่างที่เป็นรูปธรรม (อย่างไม่เห็นแก่ตัว) ซึ่งจัดทำโดยสมาชิกของรายชื่อผู้รับจดหมาย golang-nuts:

https://groups.google.com/forum/#!msg/golang-nuts/GE7a_5C5kbA/fdSnH41pOPYJ

นี่เป็นสคีมาที่แนะนำและการใช้งานฝั่งเซิร์ฟเวอร์เป็นพื้นฐานสำหรับการตรวจสอบความถูกต้อง รหัสฝั่งไคลเอ็นต์ยังขึ้นอยู่กับคุณ

(ฉันหวังว่าผู้เขียนโพสต์จะเห็นสิ่งนี้: ขอบคุณ!)

คัดลอกมา (และฟอร์แมตใหม่):


"ฉันขอแนะนำบางอย่างเช่นการออกแบบต่อไปนี้:

create table User (
 ID int primary key identity(1,1),
 Username text,
 FullName text,
 PasswordHash text,
 PasswordSalt text,
 IsDisabled bool
)

create table UserSession (
 SessionKey text primary key,
 UserID int not null, -- Could have a hard "references User"
 LoginTime <time type> not null,
 LastSeenTime <time type> not null
)
  • เมื่อผู้ใช้เข้าสู่เว็บไซต์ของคุณผ่าน POST ภายใต้ TLS ให้ตรวจสอบว่ารหัสผ่านนั้นถูกต้องหรือไม่
  • จากนั้นออกรหัสเซสชันแบบสุ่มพูดตัวอักษรและรหัส 50 อันหรือมากกว่านั้นในคุกกี้ที่ปลอดภัย
  • เพิ่มคีย์เซสชันนั้นลงในตาราง UserSession
  • จากนั้นเมื่อคุณเห็นผู้ใช้นั้นอีกครั้งแรกให้กด UserSession ตารางเพื่อดูว่า SessionKey อยู่ในนั้นด้วย LoginTime ที่ถูกต้องและ LastSeenTime และผู้ใช้จะไม่ถูกลบ คุณสามารถออกแบบได้ดังนั้นตัวจับเวลาจะทำการล้างแถวเก่าใน UserSession โดยอัตโนมัติ "

8
เรามักจะชอบไซต์ที่มีอยู่ในตัวเองที่ SO ดังนั้นคุณคิดจะโพสต์โซลูชันที่นี่ด้วยหรือไม่ ในกรณีที่การเชื่อมโยงเปลี่ยนแปลงในเวลาที่กำหนด (การเชื่อมโยงเน่าและสิ่งอื่น ... ) ผู้เข้าชมในอนาคตอาจจะดีใจเกี่ยวกับเรื่องนี้
topskip

นั่นเป็นคำถามที่ยุติธรรมใส่ไว้ด้วยความเคารพ ขอบคุณ. ฉันได้รวมวิธีแก้ปัญหาไว้ คุณคิดว่าควรรวมชื่อผู้แต่งด้วยหรือไม่ (เป็นสาธารณะ แต่ฉันสงสัยเกี่ยวกับมารยาทของตัวเลือกอย่างใดอย่างหนึ่ง)
SexxLuthor

ฉันคิดว่ามันดีเหมือนเดิม คุณไม่ได้อ้างว่าเป็น "เจ้าของ" ของตัวอย่างนี้และฉันไม่สามารถเห็นได้ว่าผู้เขียนต้นฉบับของตัวอย่างนี้ต้องการให้ทุกสำเนาต้องมีการระบุแหล่งที่มา (เฉพาะสองเซ็นต์ของฉัน)
topskip

35
ไม่ควรมีฟิลด์ "PasswordSalt" ในฐานข้อมูลของคุณเนื่องจากคุณควรใช้ bcrypt เป็นอัลกอริทึมการแฮชซึ่งสร้างเกลือโดยอัตโนมัติและรวมไว้ในแฮชที่ส่งคืน นอกจากนี้ยังใช้ฟังก์ชั่นการเปรียบเทียบเวลาคงที่
0xdabbad00

4
+1 สำหรับ bcrypt นอกจากนี้เซสชันกอริลลาที่มีคีย์ 'การเข้ารหัส' และ 'การรับรองความถูกต้อง' จะช่วยให้คุณเก็บข้อมูลเซสชันอย่างปลอดภัยโดยไม่ต้องใช้ตาราง DB
crantok


14

คุณจะใช้มิดเดิลแวร์เพื่อทำการพิสูจน์ตัวตน

คุณสามารถลองgo-http-authสำหรับการตรวจสอบสิทธิ์พื้นฐานและการแยกย่อยและgomniauthสำหรับ OAuth2

แต่วิธีการรับรองความถูกต้องขึ้นอยู่กับแอพของคุณจริงๆ

การรับรองความถูกต้องแนะนำสถานะ / บริบทใน http.Handlers ของคุณและมีการอภิปรายเกี่ยวกับเรื่องนี้เมื่อเร็ว ๆ นี้

การแก้ปัญหาที่รู้จักกันดีในการแก้ไขปัญหาบริบทคือกอริลลา / บริบทและบริบทของ Googleอธิบายไว้ที่นี่ที่นี่

ฉันสร้างโซลูชันทั่วไปเพิ่มเติมโดยไม่ต้องใช้สถานะโกลบอลในgo-on / wrapที่อาจใช้ร่วมกันหรือไม่ใช้อีกสองรายการและรวมเข้ากับมิดเดิลแวร์แบบไม่มีบริบท

wraphttpauthจัดให้มีการรวมกันของ go-http-auth กับ go-on / wrap


มีสิ่งใหม่มากมายสำหรับผู้เริ่มต้น ฉันสงสัยว่าสิ่งใดที่ผู้เริ่มต้นควรเริ่มต้นด้วย go-http-authหรือgomniauthหรือทั้งสองอย่าง
Casper

ทุกคนที่นี่ใช้ OAuth 1.0 ใน golang ไหม ConsumerKey และการตรวจสอบตามความลับ?
2888996

ฉันจะใช้งาน oAuth 1.0 ได้อย่างไร ใช้รหัสผู้บริโภคและความลับ? กรุณาช่วย. ฉันไม่ได้รับห้องสมุดเหมือนกัน
2888996

9

ตอบคำถามนี้ในปี 2561 ฉันแนะนำให้ใช้ JWT (JSON Web Token) คำตอบที่คุณทำเครื่องหมายว่าแก้ไขแล้วมีข้อเสียเปรียบซึ่งเป็นการเดินทางที่ผู้ใช้ทำ (ผู้ใช้) และด้านหลัง (เซิร์ฟเวอร์ / ฐานข้อมูล) สิ่งที่เลวร้ายยิ่งกว่านั้นหากผู้ใช้ทำคำขอบ่อยครั้งที่ต้องการรับรองความถูกต้องจะส่งผลให้คำขอจาก / ไปยังเซิร์ฟเวอร์และฐานข้อมูลป่อง เพื่อแก้ปัญหานี้ให้ใช้ JWT ซึ่งเก็บโทเค็นไว้ในส่วนท้ายของผู้ใช้ซึ่งผู้ใช้สามารถใช้งานได้ทุกเมื่อที่ต้องการการเข้าถึง / ร้องขอ ไม่จำเป็นต้องเดินทางไปยังฐานข้อมูลและการประมวลผลเซิร์ฟเวอร์เพื่อตรวจสอบความถูกต้องของโทเค็นใช้เวลาสั้น ๆ



2

จริงๆแล้วมีวิธีการตรวจสอบและเทคนิคมากมายที่คุณสามารถเชื่อมต่อกับแอปพลิเคชันของคุณและขึ้นอยู่กับตรรกะทางธุรกิจและข้อกำหนดของแอปพลิเคชัน
ตัวอย่างเช่น Oauth2, LDAP, การตรวจสอบท้องถิ่น ฯลฯ
คำตอบของฉันถือว่าคุณกำลังมองหาการรับรองความถูกต้องในท้องถิ่นซึ่งหมายความว่าคุณจัดการข้อมูลประจำตัวของผู้ใช้ในแอปพลิเคชันของคุณ เซิร์ฟเวอร์ต้องเปิดเผยชุดของ API ภายนอกที่อนุญาตให้ผู้ใช้และผู้ดูแลระบบจัดการบัญชีและวิธีที่พวกเขาต้องการระบุตัวเองไปยังเซิร์ฟเวอร์เพื่อให้เกิดการสื่อสารที่น่าเชื่อถือ คุณจะต้องสร้างตารางฐานข้อมูลของผู้ใช้ รหัสผ่านถูกแฮชเพื่อความปลอดภัยดูวิธีการเก็บรหัสผ่านในฐานข้อมูล

ให้ถือว่าข้อกำหนดของแอปเพื่อตรวจสอบสิทธิ์ผู้ใช้โดยใช้วิธีใดวิธีหนึ่งต่อไปนี้:

  • การรับรองความถูกต้องขั้นพื้นฐาน (ชื่อผู้ใช้รหัสผ่าน):
    วิธีการตรวจสอบความถูกต้องนี้ขึ้นอยู่กับข้อมูลประจำตัวผู้ใช้ที่กำหนดไว้ในส่วนหัวการอนุญาตที่เข้ารหัสใน base64 และกำหนดไว้ในrfc7617โดยทั่วไปเมื่อแอปได้รับคำขอของผู้ใช้ แฮถ้าตรงกับผู้ใช้รับรองความถูกต้องมิฉะนั้นส่งคืนรหัสสถานะ 401 ให้ผู้ใช้

  • การรับรองความถูกต้องโดยใช้ใบรับรอง:
    วิธีการรับรองความถูกต้องนี้ขึ้นอยู่กับ Digital Certificate เพื่อระบุผู้ใช้และเป็นที่รู้จักกันในชื่อ x509 auth ดังนั้นเมื่อแอปได้รับคำขอของผู้ใช้จะอ่านใบรับรองของลูกค้าและตรวจสอบว่าตรงกับใบรับรอง CA หลัก ไปที่แอป

  • ผู้ใช้โทเค็น:
    วิธีการรับรองความถูกต้องนี้ขึ้นอยู่กับโทเค็นการเข้าถึงระยะสั้นโทเค็นผู้ถือเป็นสตริงที่เข้ารหัสลับซึ่งมักจะสร้างโดยเซิร์ฟเวอร์เพื่อตอบสนองต่อคำขอเข้าสู่ระบบ ดังนั้นเมื่อแอปได้รับคำขอของผู้ใช้แอปจะอ่านการอนุญาตและตรวจสอบโทเค็นเพื่อตรวจสอบสิทธิ์ผู้ใช้

อย่างไรก็ตามฉันขอแนะนำgo-guardian สำหรับไลบรารี่การพิสูจน์ตัวตนซึ่งทำผ่านวิธีการพิสูจน์ตัวตนแบบขยายที่รู้จักกันในชื่อกลยุทธ์ โดยทั่วไปแล้ว Go-Guardian ไม่ได้เมานต์เส้นทางหรือสันนิษฐานว่าสคีมาฐานข้อมูลใด ๆ ซึ่งเพิ่มความยืดหยุ่นและช่วยให้ผู้พัฒนาสามารถตัดสินใจได้

การตั้งค่าตัวพิสูจน์ตัวตนของผู้พิทักษ์เป็นเรื่องตรงไปตรง

นี่คือตัวอย่างเต็มรูปแบบของวิธีการดังกล่าว

package main

import (
    "context"
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "sync"

    "github.com/golang/groupcache/lru"
    "github.com/gorilla/mux"
    "github.com/shaj13/go-guardian/auth"
    "github.com/shaj13/go-guardian/auth/strategies/basic"
    "github.com/shaj13/go-guardian/auth/strategies/bearer"
    gx509 "github.com/shaj13/go-guardian/auth/strategies/x509"
    "github.com/shaj13/go-guardian/store"
)

var authenticator auth.Authenticator
var cache store.Cache

func middleware(next http.Handler) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("Executing Auth Middleware")
        user, err := authenticator.Authenticate(r)
        if err != nil {
            code := http.StatusUnauthorized
            http.Error(w, http.StatusText(code), code)
            return
        }
        log.Printf("User %s Authenticated\n", user.UserName())
        next.ServeHTTP(w, r)
    })
}

func Resource(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Resource!!\n"))
}

func Login(w http.ResponseWriter, r *http.Request) {
    token := "90d64460d14870c08c81352a05dedd3465940a7"
    user := auth.NewDefaultUser("admin", "1", nil, nil)
    cache.Store(token, user, r)
    body := fmt.Sprintf("token: %s \n", token)
    w.Write([]byte(body))
}

func main() {
    opts := x509.VerifyOptions{}
    opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
    opts.Roots = x509.NewCertPool()
    // Read Root Ca Certificate
    opts.Roots.AddCert(readCertificate("<root-ca>"))

    cache = &store.LRU{
        lru.New(100),
        &sync.Mutex{},
    }

    // create strategies
    x509Strategy := gx509.New(opts)
    basicStrategy := basic.New(validateUser, cache)
    tokenStrategy := bearer.New(bearer.NoOpAuthenticate, cache)

    authenticator = auth.New()
    authenticator.EnableStrategy(gx509.StrategyKey, x509Strategy)
    authenticator.EnableStrategy(basic.StrategyKey, basicStrategy)
    authenticator.EnableStrategy(bearer.CachedStrategyKey, tokenStrategy)

    r := mux.NewRouter()
    r.HandleFunc("/resource", middleware(http.HandlerFunc(Resource)))
    r.HandleFunc("/login", middleware(http.HandlerFunc(Login)))

    log.Fatal(http.ListenAndServeTLS(":8080", "<server-cert>", "<server-key>", r))
}

func validateUser(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
    // here connect to db or any other service to fetch user and validate it.
    if userName == "stackoverflow" && password == "stackoverflow" {
        return auth.NewDefaultUser("stackoverflow", "10", nil, nil), nil
    }

    return nil, fmt.Errorf("Invalid credentials")
}

func readCertificate(file string) *x509.Certificate {
    data, err := ioutil.ReadFile(file)

    if err != nil {
        log.Fatalf("error reading %s: %v", file, err)
    }

    p, _ := pem.Decode(data)
    cert, err := x509.ParseCertificate(p.Bytes)
    if err != nil {
        log.Fatalf("error parseing certificate %s: %v", file, err)
    }

    return cert
}

การใช้งาน:

  • รับโทเค็น:
curl  -k https://127.0.0.1:8080/login -u stackoverflow:stackoverflow
token: 90d64460d14870c08c81352a05dedd3465940a7
  • รับรองความถูกต้องด้วยโทเค็น:
curl  -k https://127.0.0.1:8080/resource -H "Authorization: Bearer 90d64460d14870c08c81352a05dedd3465940a7"

Resource!!
  • รับรองความถูกต้องด้วยข้อมูลรับรองผู้ใช้:
curl  -k https://127.0.0.1:8080/resource -u stackoverflow:stackoverflow

Resource!!
  • รับรองความถูกต้องด้วยใบรับรองผู้ใช้:
curl --cert client.pem --key client-key.pem --cacert ca.pem https://127.0.0.1:8080/resource

Resource!!

คุณสามารถเปิดใช้งานวิธีการรับรองความถูกต้องหลายวิธีในครั้งเดียว คุณควรใช้วิธีการอย่างน้อยสองวิธี


1

ลองดูที่Labstack Echoซึ่งจะทำการตรวจสอบความถูกต้องของ RESTful API และแอพพลิเคชั่นส่วนหน้าในมิดเดิลแวร์ที่คุณสามารถใช้เพื่อป้องกันเส้นทาง API เฉพาะ

ตัวอย่างเช่นการตั้งค่าการรับรองความถูกต้องเบื้องต้นตรงไปตรงมาเหมือนกับการสร้าง subrouter ใหม่สำหรับ/adminเส้นทาง:

e.Group("/admin").Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
    if username == "joe" && password == "secret" {
        return true, nil
    }
    return false, nil
}))

ดูตัวเลือกการตรวจสอบมิดเดิลแวร์ของ Labstack ทั้งหมดได้ที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.