互联网服务离不开用户认证,一种认证方式是:
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。 此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{ "name": name, // 用户名 "exp": time.Now().Add(time.Hour * 2).Unix(), // 添加过期时间 }
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
实际的 JWT 大概就像下面这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTk4MDMyODEsInBhc3N3b3JkIjoiMTIzMyIsInBob25lIjoiMTU5MDIwMTUwNDMifQ.NvKsiKh37pxmRH4inKb32EXT-XkSfIC96nEX7p1RLag
它是一个很长的字符串,中间用点(.
)分隔成三个部分:Header.Payload.Signature
{
"alg": "HS256",
"typ": "JWT"
}
alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。
Payload
示例
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
`
然后,
Payload
经过Base64Url
编码,形成JSON Web Token
的第二部分,数据虽然是不可串改,但是确实透明的
例如,如果要使用HMAC SHA256
算法,将按以下方式创建签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
`
签名用于验证消息在此过程中未被更改,并且,在使用私钥签名的令牌的情况下,它还可以验证JWT
的发件人是否是它所声称的人
需要注意的是,使用签名Token
,Token
中包含的所有信息都会向用户或其他方公开,即使他们无法更改。这意味着您不应该在Token
中放置秘密信息。
在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web Token
。由于Token
是凭证,因此必须非常小心以防止出现安全问题。 一般情况下,您不应该将令牌保留的时间超过要求。
每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT
,通常在Authorization Header
中。Header
的内容应如下所示:
Authorization: Bearer <token>
在某些情况下,这可以是无状态授权机制。服务器的受保护路由将在Authorization Header
中检查有效的JWT
,如果存在,则允许用户访问 受保护的资源。如果JWT
包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。
如果在Authorization Header
中发送Token
,则跨域资源共享(CORS
)将不会成为问题,因为它不使用cookie
。
使用过程:
OpenID Connect
兼容Web
应用程序 将使用授权代码流通过/oauth/authorize
端点。Token
。Token
来访问受保护资源(如API
)。在go中,开发者可以使用"github.com/dgrijalva/jwt-go"
很简单地使用JWT功能:
首先,终端输入下面的命令下载相关的包:
go get -u "github.com/dgrijalva/jwt-go"
之后,我们就可以直接import使用jwt包了:
import (
jwt "github.com/dgrijalva/jwt-go"
"github.com/kataras/iris"
)
在之前的博客:Go iris 入门,里面提到了中间件的概念,JWT验证的方式也是一样的,我们可以自定义一个JWT 中间件来进行JWT验证:
下面的代码是一个自定义JWT中间件的简单例子,里面使用的密钥是My Secret
,(因此在加密的时候需要使用同样的密钥),加密算法是HS256
jwtHandler := jwtmiddleware.New(jwtmiddleware.Config{
//这个方法将验证jwt的token
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
//自己加密的秘钥或者说盐值
return []byte("My Secret"), nil
},
//设置后,中间件会验证令牌是否使用特定的签名算法进行签名
//如果签名方法不是常量,则可以使用ValidationKeyGetter回调来实现其他检查
//重要的是要避免此处的安全问题:https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/
//加密的方式
SigningMethod: jwt.SigningMethodHS256,
//验证未通过错误处理方式
//ErrorHandler: func(context.Context, string)
//debug 模式
//Debug: bool
})
解释:
jwtmiddleware.New是配置中间件的错误返回,是否为调试模式,机密秘钥,加密模式等
app.Use(jwtHandler.Serve) 是把中间件注册到处理程序中
注册次中间件的路由,中间件每一次都会去获取header头Authorization字段用户判断
生成加密串过程
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"nick_name": "iris",
"email":"[email protected]",
"id":"1",
"iss":"Iris",
"iat":time.Now().Unix(),
"jti":"9527",
"exp":time.Now().Add(10*time.Hour * time.Duration(1)).Unix(),
})
把token已约定的加密方式和加密秘钥加密,当然也可以使用不对称加密
tokenString, _ := token.SignedString([]byte("My Secret"))
登录时候,把tokenString返回给客户端,然后需要登录的页面就在header上面附此字符串
eg: header["Authorization"] = "bears "+tokenString
在需要验证的地方,可以插入该中间件,关于中间件的使用可以参考上一篇论文:
user := app.Party("/user")
user.Post("/register", userRegisterHandler)
user.Post("/login", userLoginHandler)
// 这里的/profile路由的返回结果是用户的个人信息,因此可以知道,token的验证必要的,因此,我们使用jwtHandler.Serve进行token的验证
user.Get("/profile", jwtHandler.Serve, userTokenHandler, userProfileHandler)
上面提到了验证的方法,那么服务端如何生成token呢?
下面是一个服务端生成token返回给用户的简单例子:
可以知道,JWT token的生成是在登录的时候发生的,因此下面是userLoginHandler
的代码:
func userLoginHandler(ctx iris.Context) {
phone := ctx.FormValue("phone")
password := ctx.FormValue("password")
/* 这里省去了对用户的验证,在实际使用过程中需要验证用户是否存在,密码是否正确 */
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"phone": phone,
"password": password,
"exp": time.Now().Add(time.Hour * 2).Unix(), // 添加过期时间为2个小时
})
// 这里的密钥和前面的必须一样
tokenString, _ := token.SignedString([]byte("My Secret"))
tokenResponse := helper.Token_Response{
Code: 200,
Msg: "登录成功!",
Data: map[string]string{"token": tokenString}}
ctx.JSON(tokenResponse)
}