用Casbin在Go中进行基于角色的基本HTTP授权

297 阅读6分钟

认证和授权是任何安全的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 == "*")

这定义了请求和策略,包含subjectobjectaction 。在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.confpolicy.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
}

在一个真实世界的应用中,我们可能会有一个包含这些用户的数据库--在这个例子中,我们将只使用这个列表。

接下来是loginlogout 的处理程序。

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.GetIntsession.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 方法,sessionManagerAuthorizer 包裹了我们的mux ,所以每个请求都需要经过这个中间件,确保所有请求都有我们的授权机制。

你可以用不同的用户登录,并使用curlpostman 等工具尝试上述的处理程序来测试。

这就是了。你可以在这里找到完整的代码。

总结

我在一个中等规模的Web应用程序中使用了casbin ,并对该库的可维护性和稳定性感到满意。翻开它的文档,casbin 似乎是一个非常强大的授权工具,以声明的方式提供了大量的访问控制模型。

我希望这个例子能够展示casbinscs 的强大功能,并再次证明Go在网络应用和一般情况下的简洁性和清晰性。

请享受授权的乐趣。)

资源