1. JWT简介
1.1 什么是JWT
JSON Web Token (JWT) 是一个开放标准 ( RFC 7519 ),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全传输信息。此信息可以验证和信任,因为它是数字签名的。JWT 可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
最常用的场景是登录授权。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。
其次还常用于信息交换。可以对 JWT 进行签名(例如,使用公钥/私钥对),所以可以确定发件人就是他们所说的那个人。
1.2 JWT和session的区别
先来看一下用JWT登录认证的过程:
-
① 客户端使用账号密码登录
-
② 服务端验证账号密码是否存在数据库,判断有没有该用户
-
③ 若存在该用户,会在服务端通过JWT生成一个token,并把token返回给客户端
-
④ 客户端收到token会把它存起来,之后每次向服务端请求都会把该token放到header
-
⑤ 服务端收到请求后判断header有没有携带token,没有则返回验证失败,即该用户没有权限
再看一下用session登录认证的过程:
-
① 客户端使用账号密码登录
-
② 服务端验证账号密码是否存在数据库,判断有没有该用户
-
③ 若存在该用户,会在服务端生成session id,并把session id返回给客户端
-
④ 客户端收到session id后会保存到cookie中,以后向服务端的请求都会带上session id
-
⑤ 服务端根据session id来判断该用户是否有权限和查看其他信息
从上面的流程可以看出jwt和session的认证过程大致相同,但是区别还是很大的:
Title 跨域问题token无状态
,token自身携带了用户的信息,可以通过加解密的方式得出,所以服务器不需要额外的空间来存储多余的信息,而且token本身只是一行字符串,占用空间极小;而session方式中,每个用户的登录信息都会保存到服务器的session中,随着用户的增多,服务器开销会明显增大分布式
,由于session要保存到服务端,当处于分布式系统中时,无法使用该方法,就算可以通过中间件的方式解决,但这样无疑增加了复杂性,而jwt方式因为无状态,更适合于分布式系统
2. JWT结构
JWT由三部分组成,分别是header
、payload
、signature
形成的形式如:xxxxx.yyyyy.zzzzz
-
header由两部分组成:
{ "alg": "HS256", //令牌使用的签名算法 "typ": "JWT" //令牌类型 }
-
payload包含了主体信息,如iss(发行人)、 exp(到期时间)、 sub(主题)、 aud(受众)等,还可以添加自定义信息:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
-
signature,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改,因此要指定一个秘钥SigningKey:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), SigningKey)
3. Go+JWT
现在在基于go语言的beego框架中实现jwt鉴权,并在中间件中插入路由拦截
-
配置文件:
# Jwt,这是我随机生成的秘钥 SigningKey = bAlc5pLZek78sOuVZm0p6L3OmY1qSIb8u3ql # Jwt token几天后到期 ExpiresAt = 10
-
JWT逻辑实现:
package adminService import ( "errors" "fmt" "github.com/beego/beego/v2/server/web" "github.com/golang-jwt/jwt/v4" "time" ) type Jwt struct { SigningKey []byte } func NewJwt() (*Jwt, error) { SigningKey, err := web.AppConfig.String("SigningKey") if err != nil { return nil, errors.New("未从配置获取到Jwt的SigningKey") } return &Jwt{SigningKey: []byte(SigningKey)}, nil } type BaseClaims struct { Email string Password string } type RegisteredClaims struct { BaseClaims BaseClaims jwt.RegisteredClaims } // 生成claims func (j *Jwt) CreateClaims(baseClaims BaseClaims) (RegisteredClaims, error) { ExpiresAt, err := web.AppConfig.Int64("ExpiresAt") if err != nil { return RegisteredClaims{}, errors.New("未从配置获取到Jwt的过期时间") } return RegisteredClaims{ BaseClaims: baseClaims, RegisteredClaims: jwt.RegisteredClaims{ Issuer: baseClaims.Email, // 发行人 Subject: "", // 主题 Audience: nil, // 用户 ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(ExpiresAt) * 24 * time.Hour)), // 到期时间 NotBefore: jwt.NewNumericDate(time.Now()), // 在此之前不可用 IssuedAt: jwt.NewNumericDate(time.Now()), // 发布时间 ID: "", // jwt的id }, }, nil } // 检查token func CheckToken(token string) (RegisteredClaims, error) { parse, err := jwt.ParseWithClaims(token, &RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New(fmt.Sprintf("签名方式有误: [%v]", token.Header["alg"])) } SigningKey, _ := web.AppConfig.String("SigningKey") return []byte(SigningKey), nil }) if parse == nil { return RegisteredClaims{}, errors.New("token为空/token有误") } if parse.Valid { if claims, ok := parse.Claims.(*RegisteredClaims); ok { return *claims, nil } else { return RegisteredClaims{}, errors.New("token解析不正确") } } else if errors.Is(err, jwt.ErrTokenMalformed) { return RegisteredClaims{}, errors.New("令牌格式不正确") } else if errors.Is(err, jwt.ErrTokenExpired) { return RegisteredClaims{}, errors.New("令牌已过期") } else if errors.Is(err, jwt.ErrTokenSignatureInvalid) { return RegisteredClaims{}, errors.New("令牌签名无效") } else if errors.Is(err, jwt.ErrTokenNotValidYet) { return RegisteredClaims{}, errors.New("令牌尚未生效") } else { return RegisteredClaims{}, err } }
-
登录逻辑:
package adminService import ( "errors" "fmt" "github.com/golang-jwt/jwt/v4" "github.com/jinzhu/gorm" "mobile-mes-api/dto/admin" "mobile-mes-api/models" "mobile-mes-api/util/cryptoUtil" "mobile-mes-api/util/log" ) func LoginService(req admin.LoginReq) (string, error) { if req.Email == "" || req.Password == "" { return "", errors.New("未输入用户名或密码") } user, err := models.GetLoginUser(req.Email) if err != nil { if err == gorm.ErrRecordNotFound { return "", errors.New("未找到该用户") } return "", err } if cryptoUtil.Encrypt(req.Password) != user.Password { return "", errors.New("密码错误") } // 登录成功,开始生成jwt的token token, err := generateToken(req) if err != nil { return "", err } return token, nil } func generateToken(req admin.LoginReq) (string, error) { j, err := NewJwt() if err != nil { return "", err } claims, err := j.CreateClaims(BaseClaims{ Email: req.Email, Password: req.Password, }) if err != nil { return "", err } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenStr, err := token.SignedString(j.SigningKey) if err != nil { log.Error(fmt.Sprintf("生成jwt的token失败,err: [%v]", err)) return "", err } return tokenStr, nil }
-
Beego插入中间件做路由鉴权:
func init() { ns := beego.NewNamespace("/v1", // ......这里写个人的路由 ) beego.AddNamespace(ns) // jwt token鉴权 beego.InsertFilter("/*", beego.BeforeExec, controllers.FilterUser) }
-
过滤逻辑:
// 路由鉴权白名单 var permissionUrl = []string{ "/v1/test/test", } func FilterUser(ctx *context.Context) { perMap := make(map[string]bool, len(permissionUrl)) for _, v := range permissionUrl { perMap[v] = true } url := ctx.Request.RequestURI if perMap[url] == true { // 不对白名单接口鉴权 return } // 执行Jwt的token鉴权 tokenStr := ctx.Input.Header(HTTP_HEADER_KEY_TOKEN) _, err := adminService.CheckToken(tokenStr) if err != nil { ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized) resp := dto.BaseResponse{ ResCode: dto.RESPONSE_STATUS_FAIL, Message: err.Error(), } res, _ := json.Marshal(resp) ctx.ResponseWriter.Write(res) return } else { return } }
效果如下:
4. uniapp+JWT
-
登录后保存token到缓存:
export default { onLoad() { let data = { email: "test.test@test.com", password: "test.test@test.com" } http.login(data).then((response) => { let token = response.data.token uni.setStorageSync('token', token) }).catch((err)=>{ }) }, }
-
在http请求处封装token识别:
const baseUrl = "http://127.0.0.1:8091/v1"; // 白名单,无需Jwt鉴权 const perUrl = new Map([ ["/test/test", true], ]) export default (url, method, data, headers) => { let token = uni.getStorageSync('token') if (!token && !perUrl.get(url)) { uni.showModal({ title: "", content: "没有权限,请重新登陆", showCancel: false }); // todo: 重定向跳转到登录页面 uni.redirectTo({ url: "/" }) return } return new Promise((resolve, reject) => { uni.request({ url: baseUrl + url, method: method, data: data, header: headers, timeout: 10000, success: (res) => { if (res) { console.log("response success:", res) resolve(res) // 把数据返回给上层调用 } }, fail: (err) => { uni.showModal({ title: "请求失败", content: "接口请求异常: " + err.errMsg, showCancel: false }); console.log("response fail:", err.errMsg) }, complete: () => { return } }); }) }
在没有token的情况下,得到的效果如下:
...