在HTTP中,基本认证(Basic access authentication)是一种用来允许网页浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证的一种登录验证方式。
虽然基本认证非常容易实现,但该方案创建在以下的假设的基础上,即:客户端和服务器主机之间的连接是安全可信的。特别是,如果没有使用SSL/TLS这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截。该方案也同样没有对服务器返回的信息提供保护。
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
OAuth2的基本流程为:
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。 此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样:
{
"name": name, // 用户名
"exp": time.Now().Add(time.Hour * 2).Unix(), // 添加过期时间
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名;
MIME
类型,此时,假设客户端尚未被验证,则客户端提供如下请求至服务器: Get /index.html HTTP/1.0
Host: www.yourhost.com
HTTP/1.0 401 Unauthorised
Server: nginx/1.0
WWW-Authenticate: Basic realm="yourhost.com"
Content-Type: text/html
Content-Length: xxx
http1.0
或1.1
规范的客户端浏览器收到401
返回值时,将自动弹出一个登录窗口,要求用户输入用户名和密码BASE64
加密方式加密,并将密文放入前一条请求信息中,则客户端发送的第一条请求信息则变成如下内容: Get /index.html HTTP/1.0
Host: www.yourhost.com
Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxx //加密串
Authorization
字段后的用户信息取出、解密,将解密后的用户名及密码与用户数据库进行比较验证, 如用户名及密码正确,服务器则根据请求,将所请求资源发送给客户端示例程序:
该示例程序首先将全局路由重定向到/admin
,在/admin
,使用authHandler
中间件进行登录处理,主要在于使用ctx.Request().BasicAuth()
函数:
// basic.go
package main
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/middleware/basicauth"
)
func main() {
app := iris.New()
authConfig := basicauth.Config{
Users: map[string]string{"myusername": "mypassword", "mySecondusername": "mySecondpassword"},
Realm: "Authorization Required", // 默认表示域 "Authorization Required"
Expires: time.Duration(30) * time.Minute,
}
authentication := basicauth.New(authConfig)
//作用范围 全局 app.Use(authentication) 或者 (app.UseGlobal 在Run之前)
//作用范围 单个路由 app.Get("/mysecret", authentication, h)
app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") })
//作用范围 Party
needAuth := app.Party("/admin", authentication)
{
//http://localhost:8080/admin
needAuth.Get("/", authHandler)
// http://localhost:8080/admin/profile
needAuth.Get("/profile", authHandler)
// http://localhost:8080/admin/settings
needAuth.Get("/settings", authHandler)
}
// open http://localhost:8080/admin
app.Run(iris.Addr(":8080"))
}
func authHandler(ctx iris.Context) {
username, password, _ := ctx.Request().BasicAuth()
//第三个参数因为中间件所以不需要判断其值,否则不会执行此处理程序
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
}
在运行上面的代码前,需要安装相关的库:
go get -u "github.com/kataras/iris"
运行程序:
go run main.go
运行结果如下图所示,访问http://localhost:8080
,将会自动弹出登录窗口要求登录
下面是一个简单的示例程序,该程序将进行github的OAUth2.0授权:
任何OAuth2(甚至是纯golang/x/net/oauth2)包可以与iris一起使用,但在这个例子中我们使用markbates’ goth:
在终端输入下面的命令安装包 :
go get github.com/markbates/goth/...
这个OAuth2示例适用于会话,因此我们需要附加会话管理器
开发人员可以使用任何第三方包添加自定义cookie编码器/解码器。
在这个例子中,我们将使用gorilla的securecookie:
go get github.com/markbates/goth/…
go get github.com/gorilla/securecookie
securecookie的示例可以在“sessions / securecookie”示例文件夹中找到。
package main
import (
"errors"
"os"
"sort"
"github.com/gorilla/securecookie" //可选,用于seesion的编码器/解码器
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/auth0"
"github.com/markbates/goth/providers/github"
)
var sessionsManager *sessions.Sessions
func init() {
//附加session管理器
cookieName := "mycustomsessionid"
// AES only supports key sizes of 16, 24 or 32 bytes.
// You either need to provide exactly that amount or you derive the key from what you type in.
// AES仅支持16,24或32字节的密钥大小。
// 您需要准确提供该字节数,或者从您键入的内容中获取密钥。
hashKey := []byte("the-big-and-secret-fash-key-here")
blockKey := []byte("lot-secret-of-characters-big-too")
secureCookie := securecookie.New(hashKey, blockKey)
sessionsManager = sessions.New(sessions.Config{
Cookie: cookieName,
Encode: secureCookie.Encode,
Decode: secureCookie.Decode,
})
}
//下面是一些辅助函数
// GetProviderName函数是用于获取提供者名称(授权应用名称)通过请求。
// 默认情况下,将从URL查询字符串中提取此授权应用名称。
// 如果您以不同的方式提供, 将自己的函数分配给返回提供者的变量用您的请求的名称。
var GetProviderName = func(ctx iris.Context) (string, error) {
//尝试从的url参数中获取provider
if p := ctx.URLParam("provider"); p != "" {
return p, nil
}
//尝试从url PATH参数“{provider}或:provider或{provider:string}或{provider:alphabetical}”获取它
if p := ctx.Params().Get("provider"); p != "" {
return p, nil
}
//尝试从上下文的每个请求存储中获取它
if p := ctx.Values().GetString("provider"); p != "" {
return p, nil
}
//如果没有找到,则返回一个带有相应错误的空字符串
return "", errors.New("you must select a provider")
}
/*
BeginAuthHandler是用于启动身份验证过程的便捷处理程序。
它希望能够从查询参数中获取提供程序的名称授权应用名称)
作为“provider”或“:provider”。
BeginAuthHandler会将用户重定向到相应的身份验证端点对于请求的provider。
请参阅https://github.com/markbates/goth/examples/main.go以查看此操作。
*/
func BeginAuthHandler(ctx iris.Context) {
url, err := GetAuthURL(ctx)
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Writef("%v", err)
return
}
ctx.Redirect(url, iris.StatusTemporaryRedirect)
}
/*
GetAuthURL使用provided的请求启动身份验证过程。
它将返回一个应该用于向用户发送的URL。
它希望能够从查询参数中获取provider的名称
作为“provider”或“:provider”或来自“provider”键的上下文值。
我建议使用BeginAuthHandler而不是执行所有这些步骤,但那完全取决于你。
*/
func GetAuthURL(ctx iris.Context) (string, error) {
providerName, err := GetProviderName(ctx)
if err != nil {
return "", err
}
provider, err := goth.GetProvider(providerName)
if err != nil {
return "", err
}
sess, err := provider.BeginAuth(SetState(ctx))
if err != nil {
return "", err
}
url, err := sess.GetAuthURL()
if err != nil {
return "", err
}
session := sessionsManager.Start(ctx)
session.Set(providerName, sess.Marshal())
return url, nil
}
// SetState设置与给定请求关联的状态字符串。
// 如果没有状态字符串与请求相关联,则会生成一个。
// 此状态发送给provider,可以在提取期间检索回调
var SetState = func(ctx iris.Context) string {
state := ctx.URLParam("state")
if len(state) > 0 {
return state
}
return "state"
}
// GetState获取回调期间provider的返回的状态。
// 这用于防止CSRF攻击,请参阅 http://tools.ietf.org/html/rfc6749#section-10.12
var GetState = func(ctx iris.Context) string {
return ctx.URLParam("state")
}
/*
CompleteUserAuth在锡上做了它所说的。 它完成了身份验证处理并从provider处获取有关用户的所有基本信息。
它希望能够从查询参数中获取provider的名称作为“provider”或“:provider”。
请参阅https://github.com/markbates/goth/examples/main.go以查看此操作。
*/
var CompleteUserAuth = func(ctx iris.Context) (goth.User, error) {
providerName, err := GetProviderName(ctx)
if err != nil {
return goth.User{}, err
}
provider, err := goth.GetProvider(providerName)
if err != nil {
return goth.User{}, err
}
session := sessionsManager.Start(ctx)
value := session.GetString(providerName)
if value == "" {
return goth.User{}, errors.New("session value for " + providerName + " not found")
}
sess, err := provider.UnmarshalSession(value)
if err != nil {
return goth.User{}, err
}
user, err := provider.FetchUser(sess)
if err == nil {
//可以找到现有session数据的用户
return user, err
}
//获取新令牌并重试获取
_, err = sess.Authorize(provider, ctx.Request().URL.Query())
if err != nil {
return goth.User{}, err
}
session.Set(providerName, sess.Marshal())
return provider.FetchUser(sess)
}
// 注销使用户session
func Logout(ctx iris.Context) error {
providerName, err := GetProviderName(ctx)
if err != nil {
return err
}
session := sessionsManager.Start(ctx)
session.Delete(providerName)
return nil
}
//一些函数助手的结尾 设置key secret 与回调方法
func main() {
goth.UseProviders(
github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), "http://localhost:3000/auth/github/callback"),
// Auth0为每个客户分配域,必须提供域以使auth0正常工作
auth0.New(os.Getenv("AUTH0_KEY"), os.Getenv("AUTH0_SECRET"), "http://localhost:3000/auth/auth0/callback", os.Getenv("AUTH0_DOMAIN")),
)
m := make(map[string]string)
m["github"] = "Github"
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
providerIndex := &ProviderIndex{Providers: keys, ProvidersMap: m}
//创建我们的应用,
//设置一个视图
//设置sessions
//并为展示设置路由器
app := iris.New()
//启动路由器
app.Get("/auth/{provider}/callback", func(ctx iris.Context) {
user, err := CompleteUserAuth(ctx)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Writef("%v", err)
return
}
ctx.ViewData("", user)
if err := ctx.View("user.html"); err != nil {
ctx.Writef("%v", err)
}
})
app.Get("/logout/{provider}", func(ctx iris.Context) {
Logout(ctx)
ctx.Redirect("/", iris.StatusTemporaryRedirect)
})
app.Get("/auth/{provider}", func(ctx iris.Context) {
//尝试让用户无需重新进行身份验证
if gothUser, err := CompleteUserAuth(ctx); err == nil {
ctx.ViewData("", gothUser)
if err := ctx.View("user.html"); err != nil {
ctx.Writef("%v", err)
}
} else {
BeginAuthHandler(ctx)
}
})
app.Get("/", func(ctx iris.Context) {
ctx.ViewData("", providerIndex)
if err := ctx.View("index.html"); err != nil {
ctx.Writef("%v", err)
}
})
// http://localhost:3000
app.Run(iris.Addr("localhost:3000"))
}
type ProviderIndex struct {
Providers []string
ProvidersMap map[string]string
}