Shanraq.org Shanraq.org
Құқық: кім нені өзгерте алады
IT

Go тілі: нөлден өз блогыңызға дейін 38-сабақ (барлығы 50)

Құқық: кім нені өзгерте алады

Go курсының отыз сегізінші сабағы. Батырманы жасыру — қорғаныс емес: бөтен адам батырманы көрмей тұрып, тікелей сұраныспен мақалаңызды қалай түзететіні өлшенді. Тексеру өңдеушіде әрі бір жерде тұрады, ал бас тартулар әртүрлі: қонақты кіруге, бөтенге 403, жоғына 404.

Не үшін керек

Өткен сабақта блог кірген адамды тани бастады. Бірақ тану — істің жартысы: енді бұл адамға не рұқсат екенін шешу қалды.

Екі сөздің айырмасын есте сақтаған жөн. Аутентификация «сіз кімсіз» деген сұраққа жауап береді — бұл пошта мен құпиясөз арқылы кіру. Авторизация басқасына жауап береді: «сізге не рұқсат» — бүгінгі сабақ сол.

Блогтағы ереже қарапайым: оқуды кез келген адам, ал өңдеу мен өшіруді тек жазған адам жасайды.

Бірден тұтас

Жаңа қалта, go mod init sabaq35. Бағдарлама серверді екі рет көтереді: алдымен әдетте қалай істейтін болса солай, сосын қалай керек болса солай.

package main

import (
	"database/sql"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"os"

	_ "modernc.org/sqlite"
)

// ErrNotAllowed — адам кірді, бірақ бұл мақала оныкі емес.
var ErrNotAllowed = errors.New("бұл сіздің мақалаңыз емес")

func main() {
	os.Remove("blog.db")
	db := open()
	defer db.Close()

	fmt.Println("== батырма көрінбейді, тексеру жоқ")
	show(db, false)

	fmt.Println()
	fmt.Println("== өңдеушідегі тексеру")
	show(db, true)
}

func show(db *sql.DB, guarded bool) {
	reset(db)
	srv := httptest.NewServer(routes(db, guarded))
	defer srv.Close()

	fmt.Println("иесі батырманы көреді: ", hasButton(srv.URL, "aigul"))
	fmt.Println("бөтен батырманы көреді:", hasButton(srv.URL, "bota"))

	fmt.Println("иесі өңдейді:          ", edit(srv.URL, "aigul", "Дала: иесі түзетті"))
	fmt.Println("бөтен өңдейді:         ", edit(srv.URL, "bota", "Дала: бөтен түзетті"))
	fmt.Println("қонақ өңдейді:         ", edit(srv.URL, "", "Дала: қонақ түзетті"))
	fmt.Println("бәрінен кейін базада:  ", title(db, "step"))
}

func routes(db *sql.DB, guarded bool) http.Handler {
	mux := http.NewServeMux()

	// Мақала беті: «өңдеу» батырмасын тек иесі көреді.
	mux.HandleFunc("GET /read/{slug}", func(w http.ResponseWriter, r *http.Request) {
		slug := r.PathValue("slug")
		owner, err := ownerOf(db, slug)
		if err != nil {
			http.NotFound(w, r)
			return
		}
		fmt.Fprintf(w, "<h1>%s</h1>", title(db, slug))
		if who(r) == owner {
			fmt.Fprint(w, `<a href="/edit/`+slug+`">өңдеу</a>`)
		}
	})

	mux.HandleFunc("POST /edit/{slug}", func(w http.ResponseWriter, r *http.Request) {
		slug := r.PathValue("slug")

		if guarded {
			if err := mayEdit(db, who(r), slug); err != nil {
				answer(w, err)
				return
			}
		}

		if _, err := db.Exec(`update articles set title = ? where slug = ?`,
			r.FormValue("title"), slug); err != nil {
			http.Error(w, "болмады", http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusSeeOther)
	})

	return mux
}

// mayEdit — кімге не рұқсат екені шешілетін жалғыз орын. Мұндай орын екеу
// болса, олар бір күні ажырасады.
func mayEdit(db *sql.DB, user, slug string) error {
	if user == "" {
		return ErrNoUser
	}
	owner, err := ownerOf(db, slug)
	if err != nil {
		return err
	}
	if owner != user {
		return ErrNotAllowed
	}
	return nil
}

// answer құқық қатесін HTTP жауабына айналдырады. Қонақты кіруге жібереді,
// бөтенге бас тартады, ал жоқ нәрсе жоқ болып қала береді.
func answer(w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, ErrNoUser):
		w.Header().Set("Location", "/login")
		w.WriteHeader(http.StatusSeeOther)
	case errors.Is(err, ErrNotAllowed):
		http.Error(w, "бұл сіздің мақалаңыз емес", http.StatusForbidden)
	default:
		http.Error(w, "табылмады", http.StatusNotFound)
	}
}

func ownerOf(db *sql.DB, slug string) (string, error) {
	var owner string
	err := db.QueryRow(`select u.name from articles a
		join users u on u.id = a.author_id
		where a.slug = ?`, slug).Scan(&owner)
	if errors.Is(err, sql.ErrNoRows) {
		return "", ErrNoArticle
	}
	if err != nil {
		log.Fatal(err)
	}
	return owner, nil
}

Көмекшілер — екінші файлда, helpers.go: ол екі адам мен иесі бар мақаланы жасайды әрі әртүрлі адамның атынан сұраныс жібереді.

Шығатыны:

== батырма көрінбейді, тексеру жоқ
иесі батырманы көреді:  иә
бөтен батырманы көреді: жоқ
иесі өңдейді:           303 
бөтен өңдейді:          303 
қонақ өңдейді:          303 
бәрінен кейін базада:   Дала: қонақ түзетті

== өңдеушідегі тексеру
иесі батырманы көреді:  иә
бөтен батырманы көреді: жоқ
иесі өңдейді:           303 
бөтен өңдейді:          403 бұл сіздің мақалаңыз емес
қонақ өңдейді:          303 → /login
бәрінен кейін базада:   Дала: иесі түзетті

Талдау

Батырманы жасыру — қорғаныс емес

Шығудың бірінші жартысына қараңыз. «Өңдеу» батырмасын бөтен адам көрмейді — үлгі оны адал көрсетпеді. Соған қарамастан мақаланы қонақ түзетті, оның тіпті кіруі де жоқ.

Себебі батырма — браузерге берілген нұсқау, құлып емес. Сұранысты онсыз да жіберуге болады: мекенжай жолынан, браузер консолінен, бір жолдық curl-дан. Бұл үшін ештеңені бұзудың қажеті жоқ.

Елестетіп көріңіз. Құлпы жоқ есіктегі «бөтен адамға кіруге болмайды» деген тақтайша. Ол адал ескертеді, әрі кіремін деген ешкімді ұстап қала алмайды.

Бұл қатенің аты бар — объектіге қауіпсіз емес тікелей сілтеме: мекенжай белгілі, тексеру жоқ, әрі бөтен нөмірді қойған адамға бөтеннің бәрі қолжетімді. Ең жиі тесіктер тізімінде ол жылдар бойы тұр.

Ереже: үлгі шешетіннің бәрін өңдеуші де шешуге тиіс. Үлгі батырманы ыңғайлылық үшін жасырады, өңдеуші құқықты қауіпсіздік үшін тексереді.

Тексеру бір жерде тұрады

Шығудың екінші жартысында mayEdit пайда болады, әрі нәтиже басқаша: бөтен адам бас тарту алады, ал базада иесінің түзетуі қалады.

Тексерудің бар екені ғана емес, оның біреу екені де маңызды:

func mayEdit(db *sql.DB, user, slug string) error {
	if user == "" {
		return ErrNoUser
	}
	owner, err := ownerOf(db, slug)
	if err != nil {
		return err
	}
	if owner != user {
		return ErrNotAllowed
	}
	return nil
}

Мұндай орын екеу болса — түзетуде және өшіруде, — олар бір күні ажырасады: біреуіне редакторға арналған тексеру қосылады, екіншісінде ұмытылады. Барлық жерден шақырылатын бір функция осыдан құтқарады.

Оның не қайтаратынына назар аударыңыз: bool емес, қате. true мен false «кіріңіз» бен «бұл сіздікі еместі» ажыратпайды, ал өңдеушіге ажырату керек.

Үш түрлі бас тарту

answer қатені жауапқа айналдырады, әрі үш жағдайдың үшеуі де әртүрлі:

  • қонаққа — /login-ге 303. Ол жаман ештеңе істеген жоқ, оған жай ғана кіру керек. Шығуда оны дәл қайда жібергені көрініп тұр;
  • бөтенге — 403. Ол кірді, біз оның кім екенін білеміз, әрі жауап «сіз танылдыңыз, бірақ сізге мұнда болмайды» дегенді білдіреді. Оны қайта кіруге шақырудың мағынасы жоқ;
  • жоққа — 404. Мақала жоқ, ал құқықтың бұған қатысы жоқ.

401 мен 403-тің айырмасы жиі шатасады. 401 Unauthorized — «таныстырыңыз», атына қарағанда керісінше көрінсе де; 403 Forbidden — «таныстыңыз, бірақ болмайды». Браузерлік қосымшаларда 401-дің орнына әдетте кіру бетіне жібереді, мұндағыдай.

Білген жөн төртінші тәсіл де бар: бөтенге 403-тің орнына 404 жауап беру. Сонда бөтен адам мұндай мақаланың бар екенін де білмейді. Блог үшін бұл артық — оқырман бәрібір барлық мақаланы көреді, — ал бөтен нобайлар мен жеке хабарлар үшін бұл дұрыс таңдау.

Тексеруді тапқаннан кейін жасайды

mayEdit-тегі рет кездейсоқ емес: алдымен «кірді ме», сосын «мақала бар ма», содан кейін ғана «оныкі ме».

Соңғы екі қадамды алмастырсаңыз, оғаштық шығады: бөтеннің жоқ мақаласына 404-тің орнына 403 келеді, әрі осы арқылы қандай мекенжайлар бос емес екенін анықтауға болады. Ұсақ нәрсе, бірақ дәл осындай ұсақтардан сурет құралады.

Иесі кім екенін форма емес, дерекқор біледі

ownerOf дерекқордан сұрайды. Бұл — жалғыз дұрыс дереккөз: сұраныспен келген кез келген дерек қолдан жасалуы мүмкін, мұны cookie-ден көрдік.

Схемада байланыс біреу: мақалада users-ке сілтейтін author_id бар. Құқықты тексеру — кәдімгі join, әрі блогтағы құқық не бөлек кестені, не қитұрқы жүйені талап етпейді.

Рөл көбейгенде — редактор, модератор — рөл кестесі де, «иесі немесе редактор» түріндегі тексеру де пайда болады. Бірақ содан бастаудың қажеті жоқ: күрделілікті онсыз болмайтын кезде қосқан жөн.

Қонаққа болмайтын беттерге арналған орама

Кіруді әр өңдеушіде тексеру — құқықты екі жерде тексерумен бірдей бақытсыздық. Ол үшін middleware туралы сабақтағы орамалар бар:

func requireLogin(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if who(r) == "" {
			http.Redirect(w, r, "/login?next="+url.QueryEscape(r.URL.Path), http.StatusSeeOther)
			return
		}
		next.ServeHTTP(w, r)
	})
}

Орама «мүлде кірді ме» деген сұраққа, ал mayEdit «бұл оныкі ме» дегенге жауап береді. Біріншісі барлық жабық бетке бірдей, екіншісі мақалаға тәуелді, сондықтан олар бөлек тұрады.

Сілтемедегі next — шағын әдептілік: кіргеннен кейін адамды басты бетке емес, бара жатқан жеріне қайтарады. Тек кейін next сіздің сайтқа апаратынын тексеріңіз: мекенжай сырттан келді, демек оған сенуге болмайды.

Сабақ картасы

Сабақ картасы: кірді ме, табылды ма, оныкі ме

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.

  1. Жасырылған батырма мақаланы неге қорғамайды?
  2. 403 401-ден несімен ерекшеленеді әрі 403-тің орнына қашан 404 жауап береді?
  3. mayEdit неге true/false емес, қате қайтарады?

Тапсырма

Міндетті. Блогқа құқық қосыңыз. Көші-қон мақалаларға author_id қосып, оны бұрынғы мақалаларға қояды. Форма мақаланы кірген адамның атынан жасайды. Түзету мен өшіру иесін тексереді: қонақты кіруге жібереді, бөтенге — 403. «Өңдеу» батырмасын бұрынғыдай тек иесі көреді — бірақ енді бұл қорғаныс емес, ыңғайлылық.

Мұның бәрі step-23 ішінде жасалған — өзіңіз жазғаннан кейін салыстырыңыз.

Қалауыңызша.

  • Тексеруді өңдеушіден алып тастап, оны тек үлгіде қалдырыңыз да, бөтен мақаланы curl арқылы қайта түзетіңіз. Сосын тексеруді қайтарыңыз.
  • Кіру бетіне next қосып, кіргеннен кейін адам бара жатқан жеріне түсетінін тексеріңіз.
  • Бөтен нобай 403 емес, 404 жауап беретін етіп жасаңыз да, нобай үшін неге бұл дұрысырақ екенін түсіндіріңіз.

Блогта бұл қайда тұрады

Осы сабақтан бастап блог бір адамның блогы болуын қояды: әр мақаланың иесі бар, ал бөтенге тиісуге болмайды.

Қарыздар. Рөл жоқ — тек «иесі ме, жоқ па». Кіру әрекетінің шектеуі де жоқ. Ал формаларды әзірге бөтен сайттан жіберуге болады: бұл — CSRF тесігі, әрі келесі сабақ дәл сол туралы.

Жауаптар

Жауаптарды көрсету
  1. Себебі батырма браузерде тұрады, ал сұранысты онсыз да жіберуге болады — мекенжай жолынан, консольден немесе curl-дан. Үлгі батырманы ыңғайлылық үшін жасырады, ал шешетін — өңдеуші.
  2. 401 «таныстырыңыз» дегенді білдіреді — адам танылмаған; 403 «танылды, бірақ болмайды» дегенді. Браузерлік қосымшаларда 401-дің орнына әдетте кіру бетіне жібереді. 403-тің орнына 404-ті объектінің бар екенінің өзі құпия болғанда береді: бөтен нобайлар, жеке хабарлар.
  3. Себебі bool себепті ажыратпайды, ал өңдеушіге әртүрлі жауап беру керек: қонақты кіруге жіберу, бөтенге 403, жоққа 404. Қате себепті алып жүреді, true/false — жоқ.

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.