认证和授权是任何安全的Web应用程序的重要组成部分。我最近完成了用Go编写的第一个严肃的Web应用程序,这是关于我在这一经历中所学到的东西的系列文章中的一部分。
在这篇文章中,我们将使用伟大的casbin库研究Go中的HTTPAuthorization 。我们还将在代码示例中使用scs进行会话管理。
下面的例子是非常基本的,但我希望它能说明如何在Go网络应用中实现授权。重点在于casbin ,所以应用程序的其他部分将是非常简约和抽象的(例如:一个没有密码的登录)。
所以,让我们来看看!
免责声明:请不要把下面的示例代码作为生产级应用程序的模板,代码的重点是清晰,而不是安全。
设置
首先,我们创建一个User 模型,其中有一些实用功能。
type User struct {
ID int
Name string
Role string
}
type Users []User
func (u Users) Exists(id int) bool {
...
}
func (u Users) FindByName(name string) (User, error) {
...
}
然后我们来看看casbin 的配置。我们将为此目的创建两个文件--一个配置文件和一个策略文件。配置文件使用PERM 元模型。PERM 代表政策、效果、请求、匹配器。
我们的auth_model.conf 有以下内容。
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
这定义了请求和策略,包含subject ,object 和action 。在HTTP情况下,这意味着主题是用户角色,对象是用户想要访问的路径,动作是请求方法(例如:GET,POST等)。
matchers 定义了策略部分的匹配方式,可以像主题那样直接匹配,也可以使用像keyMatch 这样的辅助方法,它也可以匹配通配符。当然,casbin 比这个简单的例子所显示的要强大得多。你可以用声明的方式来定义各种自定义功能,使之易于切换和维护授权配置。
当涉及到安全问题时,我通常选择最简单的解决方案,因为当事情变得复杂和不可维护时,错误就开始发生了。
在这个例子中,策略文件是一个简单的csv 文件,描述了哪个角色可以访问哪些路径等等。
policy.csv 文件看起来像这样。
p, admin, /*, *
p, anonymous, /login, *
p, member, /logout, *
p, member, /member/*, *
当然,这也是非常简单的。在这个例子中,我们简单地定义了admin 角色可以访问所有的东西,member 角色可以访问/member/ 以及logout 之后的所有东西,所有未认证的用户可以使用login 。
我喜欢这种格式,因为它保持了可维护性,即使有许多规则和用户角色。
实施
让我们从main 这个函数开始,它设置了一切,并启动了HTTP服务器。
func main() {
// setup casbin auth rules
authEnforcer, err := casbin.NewEnforcerSafe("./auth_model.conf", "./policy.csv")
if err != nil {
log.Fatal(err)
}
// setup session store
engine := memstore.New(30 * time.Minute)
sessionManager := session.Manage(engine, session.IdleTimeout(30*time.Minute), session.Persist(true), session.Secure(true))
// setup users
users := createUsers()
// setup routes
mux := http.NewServeMux()
mux.HandleFunc("/login", loginHandler(users))
mux.HandleFunc("/logout", logoutHandler())
mux.HandleFunc("/member/current", currentMemberHandler())
mux.HandleFunc("/member/role", memberRoleHandler())
mux.HandleFunc("/admin/stuff", adminHandler())
log.Print("Server started on localhost:8080")
log.Fatal(http.ListenAndServe(":8080", sessionManager(authorization.Authorizer(authEnforcer, users)(mux))))
}
这里有几件事要做。基本上,我们设置了授权规则、会话管理、用户、HTTP处理程序,并通过授权中间件和会话管理器包装的路由器来启动HTTP服务器。
让我们逐一来看看。
首先,我们用上面提到的auth_model.conf 和policy.csv 文件创建一个casbin 执行器。如果失败了,我们就关闭,因为授权规则可能出了问题。
下一步是设置sessionManager 。我们创建一个具有30分钟超时的内存会话存储和一个具有安全cookie存储的会话管理器。
createUsers 函数简单地创建了三个用户和他们的用户角色,如下图所示。
func createUsers() model.Users {
users := model.Users{}
users = append(users, model.User{ID: 1, Name: "Admin", Role: "admin"})
users = append(users, model.User{ID: 2, Name: "Sabine", Role: "member"})
users = append(users, model.User{ID: 3, Name: "Sepp", Role: "member"})
return users
}
在一个真实世界的应用中,我们可能会有一个包含这些用户的数据库--在这个例子中,我们将只使用这个列表。
接下来是login 和logout 的处理程序。
func loginHandler(users model.Users) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := r.PostFormValue("name")
user, err := users.FindByName(name)
if err != nil {
writeError(http.StatusBadRequest, "WRONG_CREDENTIALS", w, err)
return
}
// setup session
if err := session.RegenerateToken(r); err != nil {
writeError(http.StatusInternalServerError, "ERROR", w, err)
return
}
session.PutInt(r, "userID", user.ID)
session.PutString(r, "role", user.Role)
writeSuccess("SUCCESS", w)
})
}
func logoutHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := session.Renew(r); err != nil {
writeError(http.StatusInternalServerError, "ERROR", w, err)
return
}
writeSuccess("SUCCESS", w)
})
}
对于login ,我们从请求的有效载荷中获取name ,检查是否有一个具有这个名字的用户,如果有,我们就创建一个新的会话,并将用户的角色和ID放入其中。
logout 处理程序只是为用户创建一个新的、空的会话,从会话存储中删除旧会话,注销用户。
然后我们有几个处理程序,通过返回用户ID和用户角色来测试实现。这些处理程序的端点是由casbin ,如上面的policy.csv 文件所见。
func currentMemberHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid, err := session.GetInt(r, "userID")
if err != nil {
writeError(http.StatusInternalServerError, "ERROR", w, err)
return
}
writeSuccess(fmt.Sprintf("User with ID: %d", uid), w)
})
}
func memberRoleHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
role, err := session.GetString(r, "role")
if err != nil {
writeError(http.StatusInternalServerError, "ERROR", w, err)
return
}
writeSuccess(fmt.Sprintf("User with Role: %s", role), w)
})
}
func adminHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writeSuccess("I'm an Admin!", w)
})
}
通过session.GetInt 和session.GetString 我们可以获取存储在当前会话中的值。
为了使这些处理程序真正受到授权机制的保护,我们需要实现Authorizer 中间件,它包裹着路由器。
func Authorizer(e *casbin.Enforcer, users model.Users) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
role, err := session.GetString(r, "role")
if err != nil {
writeError(http.StatusInternalServerError, "ERROR", w, err)
return
}
if role == "" {
role = "anonymous"
}
// if it's a member, check if the user still exists
if role == "member" {
uid, err := session.GetInt(r, "userID")
if err != nil {
writeError(http.StatusInternalServerError, "ERROR", w, err)
return
}
exists := users.Exists(uid)
if !exists {
writeError(http.StatusForbidden, "FORBIDDEN", w, errors.New("user does not exist"))
return
}
}
// casbin rule enforcing
res, err := e.EnforceSafe(role, r.URL.Path, r.Method)
if err != nil {
writeError(http.StatusInternalServerError, "ERROR", w, err)
return
}
if res {
next.ServeHTTP(w, r)
} else {
writeError(http.StatusForbidden, "FORBIDDEN", w, errors.New("unauthorized"))
return
}
}
return http.HandlerFunc(fn)
}
}
Authorizer 中间件接收casbin 规则执行者和用户作为参数。首先,它试图从会话中获取请求用户的角色。
如果用户没有用户角色,我们将其设置为anonymous ,否则,如果用户是member ,我们通过从会话中获取userID ,并与users 列表进行核对,检查它是否是一个仍然存在的有效账户。
在这些初步检查之后,我们可以把用户的角色、请求路径和请求方法交给casbin enforcer,由它来决定是否允许具有给定角色的用户(subject )访问请求方法(action )和路径(object )所指定的行动。
如果检查失败,我们会返回一个403 错误,否则我们会简单地调用包装好的HTTP处理程序,授权用户执行这个动作。
正如上面提到的main 方法,sessionManager 和Authorizer 包裹了我们的mux ,所以每个请求都需要经过这个中间件,确保所有请求都有我们的授权机制。
你可以用不同的用户登录,并使用curl 或postman 等工具尝试上述的处理程序来测试。
这就是了。你可以在这里找到完整的代码。
总结
我在一个中等规模的Web应用程序中使用了casbin ,并对该库的可维护性和稳定性感到满意。翻开它的文档,casbin 似乎是一个非常强大的授权工具,以声明的方式提供了大量的访问控制模型。
我希望这个例子能够展示casbin 和scs 的强大功能,并再次证明Go在网络应用和一般情况下的简洁性和清晰性。
请享受授权的乐趣。)