JWT
JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token实现方式,目前多用于前后端分离项目和OAuth2.0业务场景下。
诺是不用这个JWT我们就需要去使用Cookie-Session
模式实现用户认证。
流程为:
- 用户在浏览器端填写用户名和密码,并发送给服务端
- 服务端对
用户名
和密码
校验通过后会生成一份保存当前用户相关信息的session数据
和一个与之对应的标识(通常称为session_id
) - 服务端返回响应时将上一步的
session_id
写入用户浏览器的Cookie
- 后续用户来自该浏览器的每次请求都会自动携带包含
session_id
的Cookie
- 服务端通过请求中的
session_id
就能找到之前保存的该用户那份session数据
,从而获取该用户的相关信息。
这种方案依赖于客户端(浏览器)保存 Cookie,并且需要在服务端存储用户的session数据。
在移动互联网时代,我们的用户可能使用浏览器也可能使用APP来访问我们的服务,我们的web
应用可能是前后端分开部署在不同的端口,有时候我们还需要支持第三方登录,这下Cookie-Session
的模式就有些力不从心了。
JWT
就是一种基于Token
的轻量级认证模式,服务端认证通过后,会生成一个JSON
对象,经过签名后得到一个Token
(令牌)再发回给用户,用户后续请求只需要带上这个Token
,服务端解密之后就能获取该用户的相关信息了。
安装JWT
我们使用 Go 语言社区中的 jwt 相关库来构建我们的应用,例如:https://github.com/golang-jwt/jwt
方法1
:
go get github.com/golang-jwt/jwt/v4
方法2
import "github.com/golang-jwt/jwt/v4"
使用
什么是Claims
JWT token
的 payload
部分是一个json串
,是要传递数据的一组声明,这些声明被JWT
标准称为claims
。 JWT标准里面定义的标准claim包括:
iss(Issuser)
:JWT的签发主体;sub(Subject)
:JWT的所有者;aud(Audience)
:JWT的接收对象;exp(Expiration time)
:JWT的过期时间;nbf(Not Before)
:JWT的生效开始时间;iat(Issued at)
:JWT的签发时间;jti(JWT ID)
:是JWT的唯一标识
默认Claims
如果我们直接使用JWT中默认的字段,没有其他定制化的需求则可以直接使用这个包中的和方法快速生成和解析token。
// 用于签名的字符串
var mySigningKey = []byte("liwenzhou.com")
// GenRegisteredClaims 使用默认声明创建jwt
func GenRegisteredClaims() (string, error) {
// 创建 Claims
claims := &jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // 过期时间
Issuer: "qimi", // 签发人
}
// 生成token对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 生成签名字符串
return token.SignedString(mySigningKey)
}
// ParseRegisteredClaims 解析jwt
func ValidateRegisteredClaims(tokenString string) bool {
// 解析token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if err != nil { // 解析token失败
return false
}
return token.Valid
}
自定义Claims
除了以上标准声明以外。还有因为自己的需求不同来决定:JWT中来保存那些数据。
例子:我们规定在JWT中要存储username信息,那么我们就定义一个MyClaims结构体。
// CustomClaims 自定义声明类型 并内嵌jwt.RegisteredClaims
// jwt包自带的jwt.RegisteredClaims只包含了官方字段
// 假设我们这里需要额外记录一个username字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type CustomClaims struct {
// 可根据需要自行添加字段
Username string `json:"username"`
jwt.RegisteredClaims // 内嵌标准的声明
}
自定义签名的字符串的过期时间
const TokenExpireDuration = time.Hour * 24
自定义签名
// CustomSecret 用于加盐的字符串
var CustomSecret = []byte("秋天秋天悄悄过去,留下小秘密")
生成JWT
根据自己的业务需要封装一个生成 token 的函数。
// GenToken 生成JWT
func GenToken(username string) (string, error) {
// 创建一个我们自己的声明
claims := CustomClaims{
username, // 自定义字段
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpireDuration)),
Issuer: "my-project", // 签发人
},
}
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 使用指定的secret签名并获得完整的编码后的字符串token
return token.SignedString(CustomSecret)
}
解析JWT
jwt.ParseWithClaims()
// ParseToken 解析JWT
func ParseToken(tokenString string) (*CustomClaims, error) {
// 解析token
// 如果是自定义Claim结构体则需要使用 ParseWithClaims 方法
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, err error) {
// 直接使用标准的Claim则可以直接使用Parse方法
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
return CustomSecret, nil
})
if err != nil {
return nil, err
}
// 对token对象中的Claim进行类型断言
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { // 校验token
return claims, nil
}
return nil, errors.New("invalid token")
}
在gin框架中使用JWT
获取Token渠道
我们注册一条路由/auth,对外提供获取Token的渠道
r.POST("/auth", authHandler)
定义方法
authHandler定义
func authHandler(c *gin.Context) {
// 用户发送用户名和密码过来
var user UserInfo
err := c.ShouldBind(&user)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 2001,
"msg": "无效的参数",
})
return
}
// 校验用户名和密码是否正确
if user.Username == "q1mi" && user.Password == "q1mi123" {
// 生成Token
tokenString, _ := GenToken(user.Username)
c.JSON(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"token": tokenString},
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 2002,
"msg": "鉴权失败",
})
return
}
设置中间件
// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
// 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI
// 这里假设Token放在Header的Authorization中,并使用Bearer开头
// 这里的具体实现方式要依据你的实际业务情况决定
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
c.JSON(http.StatusOK, gin.H{
"code": 2003,
"msg": "请求头中auth为空",
})
c.Abort()
return
}
// 按空格分割
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusOK, gin.H{
"code": 2004,
"msg": "请求头中auth格式有误",
})
c.Abort()
return
}
// parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
mc, err := ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 2005,
"msg": "无效的Token",
})
c.Abort()
return
}
// 将当前请求的username信息保存到请求的上下文c上
c.Set("username", mc.Username)
c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
}
}
注册路由
注册一个/home
路由
r.GET("/home", JWTAuthMiddleware(), homeHandler)
func homeHandler(c *gin.Context) {
username := c.MustGet("username").(string)
c.JSON(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"username": username},
})
}
以上是自己定义的组件,但是咱们可以使用,使用Github上别人封装好的包,比如https://github.com/appleboy/gin-jwt。用法是直接调用API就成。
总结一下
这种方式是最为基本的,在go语言中Token。应用非常广泛。而以上的token是最为基础的Token,在官方文案里有说明这种Token名字叫access Token
。
我在这里简单说明一下:
访问令牌(acces Token)是用于访问受保护资源的凭据。在上面的文章中有写道。传统的访问运用的Cookie-Session
的模式,但是想必大家,肯定有所耳闻,这种模式下的cookie值并不安全。在抓包工具下。会直接暴漏。
为了增加安全性,此时就需要对信息进行加密。
访问令牌(access Token)
是一个是一个字符串,表示一个授权的客户端。这个字符串对于客户端来说是看不清的,无法直接识别。
令牌中间件是由资源服务器和授权强制执行服务器,授权持续时间和影响范围给所有者。
令牌的主要作用就是就是作为检索的标识符
,或者,携带某种验证
方式(一些数据和一个签名
)(类似于校验和
)。
在Token中,有一个抽象层,这个抽象层,会赋予资源服务器权限(单一实现)。这个抽象层会提供一个各种方法的验证身份的方法。
访问令牌可以具有不同的格式、结构和方法基于资源的利用率(例如,加密属性)服务器安全要求。访问令牌属性和访问受保护资源的方法不在本文的范围之内此规范和由配套规范定义。
我简单而言就是说对于token在保护信息的作用上,又可以实现类似Java
的springAOP
的拦截验证的机制。
我后续会给一个详细的Token的介绍。