英文:
Conditional rendering of HTML in Golang layout.tpl by session variable
问题
我使用Gorilla sessions(通过negroni-sessions)将我的用户会话存储在cookie中。我还使用github.com/unrolled/render
来进行HTML模板渲染:
main.go:
package main
import (
...
"github.com/codegangsta/negroni"
"github.com/goincremental/negroni-sessions"
"github.com/goincremental/negroni-sessions/cookiestore"
"github.com/julienschmidt/httprouter"
"github.com/unrolled/render"
...
)
func init() {
...
ren = render.New(render.Options{
Directory: "templates",
Layout: "layout",
Extensions: []string{".html"},
Funcs: []template.FuncMap{TemplateHelpers},
IsDevelopment: false,
})
...
}
func main() {
...
router := httprouter.New()
router.GET("/", HomeHandler)
// 添加会话存储
store := cookiestore.New([]byte("my password"))
store.Options(sessions.Options{
//MaxAge: 1200,
Domain: "",
Path: "/",
})
n := negroni.New(
negroni.NewRecovery(),
sessions.Sessions("cssession", store),
negroni.NewStatic(http.Dir("../static")),
)
n.UseHandler(router)
n.Run(":9000")
}
如上所示,我使用一个名为layout.html的主HTML模板,当任何页面渲染时都会包含在内,比如我的主页:
package main
import (
"html/template"
"github.com/julienschmidt/httprouter"
)
func HomeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var model = struct {
CatalogPicks []PromotionalModelList
ClearanceItems []Model
}{
CatalogPicks: GetCatalogPicks(),
ClearanceItems: GetClearanceItems(),
}
ren.HTML(w, http.StatusOK, "home", model)
}
在我的layout.html主HTML模板中,我想要渲染一个管理员菜单,但只有当前用户是管理员时才显示:
layout.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{ template "title" . }}</title>
...
</head>
<body>
...
<!--Main Menu-->
<nav class="menu">
<ul class="catalog">
<li class="has-submenu">
{{ RenderMenuCategories }}
</li>
<li><a href="javascript:void(0)">Blog</a></li>
<li><a href="javascript:void(0)">Company</a></li>
{{ RenderAdminMenu }}
</ul>
</nav>
...
我的问题是上面的模板助手函数RenderAdminMenu()
无法访问HTTP请求对象,因此无法访问User会话对象以确定用户是否为管理员。
我可以通过主页处理程序将User对象传递到模板上下文中,并在if
语句中使用RenderAdminMenu()函数,像这样:
{{ if .User.IsAdmin }}
{{ RenderAdminMenu }}
{{ end }}
...但是由于我使用了主模板,我将不得不在网站上的每个网页中都这样做。有没有更高效的方法?
我在想也许可以从RenderAdminMenu()
(或layout.html)中访问某种全局的Context
对象,其中包含Request
的详细信息(就像在ASP.NET中一样)。
英文:
I use Gorilla sessions (via negroni-sessions) to store my user sessions in cookies. I also use github.com/unrolled/render
for my HTML template rendering:
main.go:
package main
import (
...
"github.com/codegangsta/negroni"
"github.com/goincremental/negroni-sessions"
"github.com/goincremental/negroni-sessions/cookiestore"
"github.com/julienschmidt/httprouter"
"github.com/unrolled/render"
...
)
func init() {
...
ren = render.New(render.Options{
Directory: "templates",
Layout: "layout",
Extensions: []string{".html"},
Funcs: []template.FuncMap{TemplateHelpers},
IsDevelopment: false,
})
...
}
func main() {
...
router := httprouter.New()
router.GET("/", HomeHandler)
// Add session store
store := cookiestore.New([]byte("my password"))
store.Options(sessions.Options{
//MaxAge: 1200,
Domain: "",
Path: "/",
})
n := negroni.New(
negroni.NewRecovery(),
sessions.Sessions("cssession", store),
negroni.NewStatic(http.Dir("../static")),
)
n.UseHandler(router)
n.Run(":9000")
}
As you can see above, I use a layout.html master HTML template which is included when any page renders, like my home page:
package main
import (
"html/template"
"github.com/julienschmidt/httprouter"
)
func HomeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var model = struct {
CatalogPicks []PromotionalModelList
ClearanceItems []Model
}{
CatalogPicks: GetCatalogPicks(),
ClearanceItems: GetClearanceItems(),
}
ren.HTML(w, http.StatusOK, "home", model)
}
In my layout.html master HTML template, I want to render an admin menu but only if the current user is an admin:
layout.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{ template "title" . }}</title>
...
</head>
<body>
...
<!--Main Menu-->
<nav class="menu">
<ul class="catalog">
<li class="has-submenu">
{{ RenderMenuCategories }}
</li>
<li><a href="javascript:void(0)">Blog</a></li>
<li><a href="javascript:void(0)">Company</a></li>
{{ RenderAdminMenu }}
</ul>
</nav>
...
My issue is that the above template helper function RenderAdminMenu()
doesn't have access to the HTTP Request object and therefore cannot access the User session object to determine if the user is admin.
I can pass the User object into the template context via the Home page handler, and use an if
statement RenderAdminMenu() function, like this
{{ if .User.IsAdmin }}
{{ RenderAdminMenu }}
{{ end }}
...but since I am using a master template, I would have to do that from every web page on the site. Is there a more efficient way?
I was thinking perhaps there might be a way to access some kind of global Context
object from within RenderAdminMenu()
(or layout.html) which contains the Request
details (like you can in ASP.NET)
答案1
得分: 3
有几件事情你需要做来将这些内容联系在一起。我不会展示一个完整的示例,因为它会相当冗长,并且可能与你的代码不匹配(你还没有发布代码)。但是它将包含基本的构建块:如果你遇到困难,请提出一个直接的问题和一个代码片段,你将得到一个更直接的答案
-
在一个[登录]处理程序中编写一些中间件或逻辑,当用户登录时将用户数据保存在会话中。一个userID、email和admin布尔值就足够了。例如:
// 在你的登录处理程序中,一旦你从数据库中检索到用户并匹配他们的密码哈希(当然是scrypt!)。 session.Values["user"] = &youruserobject err := session.Save(r, w) if err != nil { // 抛出一个HTTP 500错误 }
注意:根据gorilla/sessions文档,记得要gob.Register(&youruserobject{})
来存储你自己的类型。
-
编写一个帮助函数,在从会话中提取类型时进行类型断言,例如:
var ErrInvalidUser = errors.New("在会话中存储的用户无效") func GetUser(session *sessions.Session) (*User, error) { // 你可以将map的键设置为常量,以避免拼写错误/错误 user, ok := session.Values["user"].(*User) if !ok || user == nil { return nil, ErrInvalidUser } return user, nil } // 在提供用户内容的处理程序中使用它 session, err := store.Get("yoursessionname", r) if err != nil { // 抛出一个HTTP 500错误 } user, err := GetUser(session) if err != nil { // 重新定向回登录页面或显示一个HTTP 403 Forbidden等。 }
-
编写一个函数来检查返回的用户是否是管理员:
func IsAdmin(user *User) bool { if user.Admin == true && user.ID != "" && user.Email != "" { return true } return false }
-
将其传递给模板:
err := template.Execute(w, "sometemplate.html", map[string]interface{}{ "admin": IsAdmin(user), "someotherdata": someStructWithData, } // 在你的模板中... {{ if .admin }}{{ template "admin_menu" }}{{ end }}
还要确保为会话cookie设置身份验证密钥(阅读gorilla文档),最好是一个加密密钥,并且使用Secure: true标志通过HTTPS提供你的网站。
请记住,上述方法也是简化的:如果你在数据库中取消用户的管理员标志,应用程序将在他们的会话持续时间内继续将其检测为管理员。默认情况下,这可以是7天,所以如果你处于一个风险环境中,管理员变动是一个真正的问题,那么可能会支付更短的会话时间或者在IsAdmin
函数中访问数据库以确保安全。如果这是一个个人博客,只有你一个人,那就不用太担心。
添加:如果你想直接将User对象传递给模板,你也可以这样做。请注意,在处理程序/中间件中这样做比在模板逻辑中这样做更高效。你还可以获得更多错误处理的灵活性,并且可以选择在会话中不包含任何内容时“提前退出”-即,如果会话不包含任何内容,你可以触发一个HTTP 500错误,而不是渲染一半的模板或在模板中放置大量的逻辑来处理nil
数据。
在将其传递给模板之前,仍然需要将你的User对象(或等效对象)存储在会话中,并从session.Values
中检索它。
func GetUser(r *http.Request) *User {
session, err := store.Get("yoursessionname", r)
if err != nil {
// 抛出一个HTTP 500错误
}
if user, ok := session.Values["user"].(*User); ok {
return user
}
return nil
}
// 在处理程序本身中
err := template.Execute(w, "sometemplate.html", map[string]interface{}{
"user": GetUser(r),
"someotherdata": someStructWithData,
}
// 在你的模板中...
{{ if .User.admin }}{{ template "admin_menu" }}{{ end }}
英文:
There's a few things you need to do to tie this together. I'm not going to show a complete example as it would be both fairly lengthy and may not match your code (which you haven't posted). It will contain the basic building blocks though: if you get stuck, come back with a direct question and a code snippet and you'll get a more direct answer
-
Write some middleware or logic in a
此处为隐藏的内容注册登录后,方可查看登录// In your login handler, once you've retrieved the user & // matched their password hash (scrypt, of course!) from the DB. session.Values["user"] = &youruserobject err := session.Save(r, w) if err != nil { // Throw a HTTP 500 }
Note: remember that you need to gob.Register(&youruserobject{})
as per the gorilla/sessions docs if you want to store your own types.
-
Write a helper to type-assert your type when you pull it out of the session, e.g.
var ErrInvalidUser= errors.New("invalid user stored in session") func GetUser(session *sessions.Session) (*User, error) { // You can make the map key a constant to avoid typos/errors user, ok := session.Values["user"].(*User) if !ok || user == nil { return nil, ErrInvalidUser } return user, nil } // Use it like this in a handler that serves user content session, err := store.Get("yoursessionname", r) if err != nil { // Throw a HTTP 500 } user, err := GetUser(session) if err != nil { // Re-direct back to the login page or // show a HTTP 403 Forbidden, etc. }
-
Write something to check if the returned user is an admin:
func IsAdmin(user *User) bool { if user.Admin == true && user.ID != "" && user.Email != "" { return true } return false }
-
Pass that to the template:
err := template.Execute(w, "sometemplate.html", map[string]interface{}{ "admin": IsAdmin(user), "someotherdata": someStructWithData, } // In your template... {{ if .admin }}{{ template "admin_menu" }}{{ end }}
Also make sure you're setting an authentication key for your session cookies (read the gorilla docs), preferably an encryption key, and that you're serving your site over HTTPS with the Secure: true flag set as well.
Keep in mind that the above method is also simplified: if you de-flag a user as admin in your DB, the application will continue to detect them as an administrator for as long as their session lasts. By default this can be 7 days, so if you're in a risky environment where admin churn is a real problem, it may pay to have really short sessions OR hit the DB inside the IsAdmin
function just to be safe. If it's a personal blog and it's just you, not so much.
Added: If you want to pass the User object directly to the template, you can do that too. Note that it's more performant to do it in your handler/middleware than it is in the template logic. You also get the flexibility of more error handling, and the option of "bailing out" earlier - i.e. if the session contains nothing, you can fire up a HTTP 500 error rather than rendering half a template or having to put lots of logic in your template to handle nil
data.
You still need to store your User
object (or equivalent) in the session, and retrieve it from session.Values
before you can pass it to the template.
func GetUser(r *http.Request) *User {
session, err := store.Get("yoursessionname", r)
if err != nil {
// Throw a HTTP 500
}
if user, ok := session.Values["user"].(*User); ok {
return user
}
return nil
}
// In the handler itself
err := template.Execute(w, "sometemplate.html", map[string]interface{}{
"user": GetUser(r),
"someotherdata": someStructWithData,
}
// In your template...
{{ if .User.admin }}{{ template "admin_menu" }}{{ end }}
答案2
得分: 1
看起来你在模板或模板辅助函数中无法访问Request
上下文(所以我接受了上面的答案)。我的解决方案是创建一个Page
结构体,将其作为每个模板的上下文传递。它包含Content
作为通用接口,User
对象以及其他有用的参数:
// Page holds the model to be rendered for every HTTP handler.
type Page struct {
MetaTitle string
User User
HeaderStyles string
HeaderScripts string
FooterScripts string
Content interface{}
}
func (pg *Page) Init(r *http.Request) {
if pg.MetaTitle == "" {
pg.MetaTitle = "This is the default <title> content for the page!"
}
user, _ := GetUserFromSession(r)
pg.User = *user
if user.IsAdmin() {
pg.HeaderStyles += `<link href="/css/libs/summernote/summernote.css" rel="stylesheet">`
pg.FooterScripts += `<script src="/js/libs/summernote/summernote.min.js"></script>`
}
}
Init
方法允许我设置默认值并更轻松地使用Page
结构体,如果只需要页面的Content
,只需指定它即可:
package main
import (
"html/template"
"github.com/julienschmidt/httprouter"
)
func HomeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var model = struct {
CatalogPicks []PromotionalModelList
ClearanceItems []Model
}{
CatalogPicks: GetCatalogPicks(),
ClearanceItems: GetClearanceItems(),
}
pg := &Page{Content: model}
pg.Init(r)
ren.HTML(w, http.StatusOK, "home", pg)
}
以上是你要翻译的内容。
英文:
It seems you cannot access the Request
context from a template or a template helper function afterall (so I accepted the answer above). My solution was to create a Page
struct that I pass as the context for every template. It contains the Content
as a generic interface, the User
object, as well as other useful parameters:
//Page holds the model to be rendered for every HTTP handler.
type Page struct {
MetaTitle string
User User
HeaderStyles string
HeaderScripts string
FooterScripts string
Content interface{}
}
func (pg *Page) Init(r *http.Request) {
if pg.MetaTitle == "" {
pg.MetaTitle = "This is the default <title> content for the page!"
}
user, _ := GetUserFromSession(r)
pg.User = *user
if user.IsAdmin() {
pg.HeaderStyles += `<link href="/css/libs/summernote/summernote.css" rel="stylesheet">`
pg.FooterScripts += `<script src="/js/libs/summernote/summernote.min.js"></script>`
}
}
The Init method allows me to set defaults and use the Page struct more easily, by specifying only the Content
for the page, if that's all I need:
package main
import (
"html/template"
"github.com/julienschmidt/httprouter"
)
func HomeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var model = struct {
CatalogPicks []PromotionalModelList
ClearanceItems []Model
}{
CatalogPicks: GetCatalogPicks(),
ClearanceItems: GetClearanceItems(),
}
pg := &Page{Content: model}
pg.Init(r)
ren.HTML(w, http.StatusOK, "home", pg)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论