人们如何在Go中管理身份验证?

206

对于那些使用Go构建RESTful API和JS前端应用程序的人,您如何管理身份验证?您是否使用任何特定的库或技术?

我很惊讶地发现关于这个话题的讨论非常少。我记住了像下面这样的答案,并试图避免开发自己的实现:

在ASP.Net中的身份验证表单

每个人都会分别编写他们自己的解决方案吗?


6
身份验证取决于您所使用的应用程序类型,没有一种通用解决方案。此外,这是一个难以解决的问题。这可能是为什么您找不到任何确切的文档的原因。 - jimt
25
谢谢你的快速回复。我理解了,但是大多数语言和框架已经提出了身份验证解决方案,覆盖了大多数应用程序共享的最常见的身份验证要求,并且有广泛的社区参与和支持。我同意这是一个困难的问题。难道不是通过合作努力才能获得最大的利益吗?(这不是抱怨,因为这是开源的,而是更多地观察到我们都在重新发明轮子。 :)) - SexxLuthor
15
@jimt 这个问题很难,这就更加重要了,需要提供一个规范的解决方案,让我们这些凡人不会出错。 - tymtam
我投票关闭此问题,因为它是一个调查问题,与主题无关。 - Jonathan Hall
7个回答

123
我知道这个问题被大量浏览——并且有一个受欢迎的问题徽章——所以我知道这个主题有很多潜在的兴趣,许多人正在询问完全相同的事情,但在互联网上找不到答案。
大部分可用信息都是文本等效物,类似于手势挥动,留给“读者练习”。;)
然而,我最终找到了一个具体的例子,由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
)

当用户通过TLS下的POST登录您的网站时,请确定密码是否有效。然后发出一个随机的会话密钥,例如50个或更多的加密字符,并将其放入安全Cookie中。将该会话密钥添加到UserSession表中。然后,当您再次看到该用户时,请首先访问UserSession表,以查看SessionKey是否在其中具有有效的LoginTime和LastSeenTime,且未删除用户。您可以设计它,使计时器自动清除UserSession中旧的行。

8
在SO这里,我们倾向于喜欢一个自包含的网站,所以您介意在这里发布解决方案吗?以防链接随着时间的推移发生更改(链接失效和其他情况……)未来的访问者可能会对此感到高兴。 - topskip
这是一个合理的问题,非常感谢。我已经包含了解决方案,请问您认为作者的姓名也应该包括在内吗?(它是公开的,但我对两种选项的礼仪有些疑惑。) - SexxLuthor
我认为现在这样很好。你没有声称是这段代码的“所有者”,我也看不出这段代码的原始作者要求每个副本都需要署名(仅代表我的个人意见)。 - topskip
40
您的数据库中不应该有"PasswordSalt"字段,因为您应该使用bcrypt作为哈希算法,它会自动创建并包含盐在返回的哈希值中。同时使用一个恒定时间比较函数。 - 0xdabbad00
4
支持bcrypt。此外,使用gorilla sessions的“encryption”和“authentication”密钥可以让您安全地存储会话信息,而无需使用数据库表。 - crantok
这种模式基本上就是自2002年和.NET 1.1以来的asp.net成员资格。一个区别是它不使用DB来进行用户会话(大多数其他语言中的常见性能瓶颈,如Python和PHP会话)。相反,它依靠解密cookie来保持用户的有效状态。如果运行超过1个Web服务器,则可以选择使用共享TCP端口或数据库共享服务器端有状态信息。后者在规模上是主要的瓶颈,而前者则具有单点故障。 - eduncan911

21

14

您可以使用中间件进行身份验证。

对于基本身份验证和摘要身份验证,您可以尝试go-http-auth,对于OAuth2,您可以尝试gomniauth

但是,如何进行身份验证确实取决于您的应用程序。

身份验证将状态/上下文引入了您的http.Handlers中,并且最近有人对此进行了一些讨论。

已知的解决上下文问题的解决方案包括gorilla/contextgoogle context ,其中在此处 进行了介绍。

我开发了一个更通用的解决方案,无需全局状态,即go-on/wrap,它可以与不需要上下文的中间件一起使用或单独使用,并且可以很好地集成。

wraphttpauth 为go-http-auth与go-on/wrap的集成提供了支持。


初学者面对很多新事物,我想知道初学者应该从哪些方面入手。是go-http-auth还是gomniauth或者两者都要学习? - Casper
这里有人在golang中实现过OAuth 1.0吗?基于ConsumerKey和Secret的身份验证? - user2888996
我该如何实现oAuth 1.0?使用Consumer Key和secret吗?请帮忙。我找不到相应的库。 - user2888996

13

回答于2018年。我建议使用JWT(JSON Web Token)。您标记为已解决的答案有缺点,即它在前端(用户)和后端(服务器/数据库)之间进行了往返。更糟糕的是,如果用户频繁请求需要认证,将导致服务器和数据库之间的请求量增加。使用JWT可以解决这个问题,它将令牌存储在用户端,用户可以随时使用该令牌进行访问/请求。不需要到数据库和服务器处理来检查令牌有效性,所需时间很短。


这篇文章指出了这种策略的一些缺陷:https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens。简而言之:一个 cookie 只有 6 个字节,而 JWT 在传输时需要 300 个字节。你已经在访问数据库了,JWTs 只有在向多个微服务传递凭据时才变得(有趣?必要?)。 - Dan Esparza

11

说实话,有很多您可以将其集成到应用程序中的身份验证方法和技术,这取决于应用程序的业务逻辑和要求。
例如Oauth2、LDAP、本地认证等。
我的回答假定您正在寻找本地身份验证,这意味着您在应用程序中管理用户的身份。
服务器必须公开一组外部API,允许用户和管理员管理帐户以及他们想要如何标识自己以实现可信通信。
您最终将创建一个包含用户信息的DB表。
密码经过哈希处理,以确保安全性,请参见如何在数据库中存储密码

假设应用程序要求根据以下方法之一对用户进行身份验证:

  • 基本身份验证(用户名、密码):
    此身份验证方法依赖于Authorization头中编码为base64的用户凭据集,定义在rfc7617中。当应用程序接收到用户请求时,会对授权进行解码并重新对密码进行哈希,以与DB哈希进行比较,如果匹配,用户将被认证,否则返回401状态代码给用户。

  • 基于证书的身份验证:
    此身份验证方法依赖于数字证书来识别用户,也称为x509身份验证。因此,当应用程序接收到用户请求时,它会读取客户端证书并验证它是否与提供给应用程序的CA根证书匹配。

  • 承载令牌:
    该身份验证方法依赖于短期访问令牌。Bearer token是一个加密字符串,通常由服务器在响应登录请求时生成。因此,当应用程序接收到用户请求时,它会读取授权并验证令牌以对用户进行身份验证。

但是,我推荐go-guardian作为身份验证库,它通过一组可扩展的身份验证方法(称为策略)进行身份验证。基本上,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!!
您可以同时启用多种身份验证方法。通常应该使用至少两种方法。

6
另一个处理cookie身份验证的开源软件包是httpauth。(顺便说一下,这是我写的)

1

看一下 Labstack Echo - 它将 RESTful API 和前端应用程序的身份验证封装为中间件,您可以使用它来保护特定的 API 路由。

例如,设置基本身份验证只需为 /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的所有中间件认证选项。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接