人们如何在Go中管理身份验证?[关闭]


187

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

我很惊讶地发现很少对此进行讨论。我谨记以下答案,并在尝试避免开发自己的实现:

ASP.Net中的身份验证表单

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


5
身份验证在很大程度上取决于您所追求的应用程序类型。没有一种万能的解决方案。另外,这是一个很难解决的问题。这可能就是为什么您找不到任何结论性文档的原因。
2014年

21
嘿,谢谢您的快速回复。可以理解,但是大多数语言和框架都提供了身份验证解决方案,该解决方案涵盖了大多数应用程序共享的最常见的身份验证要求,并且具有广泛的社区参与和支持。我同意这是一个难题。这些不是从合作中受益最大吗?(这不是抱怨,因为它是开源的,但更多的是我们都在重新发明轮子。:)
SexxLuthor 2014年

12
@jimt这是一个棘手的问题,这使得为我们的凡人提供一种不会出错的圆锥形解决方案变得更加重要。
tymtam '16

我投票关闭该问题为离题,因为这是一个民意测验问题。
Flimzy

Answers:


115

这个问题有很多见解,并且有一个“热门问题”标志,所以我知道这个主题有很多潜在的兴趣,而且很多人都在问同样的事情,而没有在Interwebs上找到答案。

大部分可用信息会产生与手形波浪状文字相同的文字,作为“阅读者的练习”。;)

但是,我终于找到了一个具体的示例,(通常)由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,并且User不会被删除。您可以对其进行设计,以便计时器自动清除UserSession中的旧行。”

8
我们倾向于在S​​O这样一个自包含的站点,所以您也介意在此处发布解决方案吗?万一链接在适当的时候发生变化(链接腐烂以及其他...),将来的访问者可能对此感到高兴。
topskip 2014年

尊敬的说,这是一个公平的问题。谢谢。我已经包含了解决方案;您认为还应该包括作者的名字吗?(这是公开的,但我想知道这两种方法的礼节。)
SexxLuthor 2014年

我认为这很好。您并没有声称自己是此代码段的“所有者”,而且我看不到此代码段的原始作者要求每个副本都需要一个属性。(只有我的两分钱)。
topskip 2014年

35
数据库中应该没有“ PasswordSalt”字段,因为您应该将bcrypt用作哈希算法,该算法会自动创建盐并将其包含在返回的哈希中。还可以使用恒定时间比较功能。
0xdabbad00 2015年

4
+1表示bcrypt。此外,大猩猩会话及其“加密”和“认证”密钥将使您无需使用数据库表即可安全地存储会话信息。
crantok


14

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

您可以尝试使用go-http-auth进行基本身份验证和摘要身份验证,使用gomniauth进行OAuth2。

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

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

上下文问题的著名解决方案是此处描述的大猩猩/上下文Google上下文

我提出了一个更通用的解决方案,而无需在go / on / wrap中使用全局状态,该状态可以一起使用,也可以不使用其他两个状态,并且可以与上下文无关的中间件很好地集成。

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


初学者有很多新事物。我想知道初学者应该从什么样的角度着手。go-http-authgomniauth或两者?
卡斯珀

这里有人在golang中实现OAuth 1.0吗?基于ConsumerKey和Secret的身份验证?
user2888996

如何实施oAuth 1.0?使用消费者密钥和机密吗?请帮忙。我没有得到任何相同的图书馆。
user2888996

10

在2018年回答这个问题。我建议使用JWT(JSON Web令牌)。您标记为已解决的答案有一个缺点,那就是它在前面(用户)和后面(服务器/ db)进行的行程。如果用户频繁发出需要身份验证的请求,更糟的是,这将导致来自服务器和数据库的请求过大。为了解决这个问题,使用JWT将令牌存储在用户端,用户可以在需要访问/请求时随时使用它。无需前往数据库和服务器处理来检查令牌有效性,只需很短的时间。



2

老实说,您可以将许多身份验证方法和技术安装到应用程序中,这取决于应用程序的业务逻辑和要求。
例如Oauth2,LDAP,本地身份验证等。
我的答案假设您正在寻找本地身份验证,这意味着您将在应用程序中管理用户的身份。服务器必须公开一组允许用户和管理员使用的外部API。管理帐户以及他们如何向Server标识自己的身份以实现可信任的通信。您最终将创建一个保存用户信息的数据库表。出于安全目的对密码进行哈希处理的位置,请参阅如何在数据库中存储密码

让我们假设应用程序要求基于以下方法之一对用户进行身份验证:

  • 基本身份验证(用户名,密码):
    此auth方法取决于在base64中编码并在rfc7617中定义的Authorization标头中的用户凭证集,基本上是在应用程序接收到用户请求其解码授权并重新哈希密码以在DB中进行比较时,如果匹配,则哈希值经过用户验证,否则返回401状态代码给用户。

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

  • 承载令牌:
    此身份验证方法取决于短期访问令牌。承载令牌是一个神秘字符串,通常由服务器根据登录请求生成。因此,当应用收到用户请求时,它会读取授权并验证令牌以验证用户身份。

但是,我建议 身份验证库使用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!!

您可以一次启用多种身份验证方法。您通常应至少使用两种方法


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的所有中间件身份验证选项。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.