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
跨域问题
,cookie无法跨域,而token没有使用cookie,所以jwt方式不存在跨域问题,跨域问题常见于小程序开发,所以移动端特别适合使用jwt技术 token无状态
,token自身携带了用户的信息,可以通过加解密的方式得出,所以服务器不需要额外的空间来存储多余的信息,而且token本身只是一行字符串,占用空间极小;而session方式中,每个用户的登录信息都会保存到服务器的session中,随着用户的增多,服务器开销会明显增大分布式
,由于session要保存到服务端,当处于分布式系统中时,无法使用该方法,就算可以通过中间件的方式解决,但这样无疑增加了复杂性,而jwt方式因为无状态,更适合于分布式系统
2. JWT结构#
JWT由三部分组成,分别是header
、payload
、signature
形成的形式如:xxxxx.yyyyy.zzzzz
header由两部分组成:
代码语言:javascript复制{
"alg": "HS256", //令牌使用的签名算法
"typ": "JWT" //令牌类型
}
payload包含了主体信息,如iss(发行人)、 exp(到期时间)、 sub(主题)、 aud(受众)等,还可以添加自定义信息:
代码语言:javascript复制{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
signature,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改,因此要指定一个秘钥SigningKey:
代码语言:javascript复制HMACSHA256(base64UrlEncode(header) "." base64UrlEncode(payload), SigningKey)
3. Go JWT#
现在在基于go语言的beego框架中实现jwt鉴权,并在中间件中插入路由拦截
配置文件:
代码语言:javascript复制# Jwt,这是我随机生成的秘钥
SigningKey = bAlc5pLZek78sOuVZm0p6L3OmY1qSIb8u3ql
# Jwt token几天后到期
ExpiresAt = 10
JWT逻辑实现:
代码语言:javascript复制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
}
}
登录逻辑:
代码语言:javascript复制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插入中间件做路由鉴权:
代码语言:javascript复制func init() {
ns := beego.NewNamespace("/v1",
// ......这里写个人的路由
)
beego.AddNamespace(ns)
// jwt token鉴权
beego.InsertFilter("/*", beego.BeforeExec, controllers.FilterUser)
}
过滤逻辑:
代码语言:javascript复制// 路由鉴权白名单
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到缓存:
代码语言:javascript复制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识别:
代码语言:javascript复制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') // 在前端判断是否能拿到token,若不能,则返回登录页面重新登陆
if (!token && !perUrl.get(url)) {
uni.showModal({
title: "",
content: "没有权限,请重新登陆",
showCancel: false
});
// todo: 重定向跳转到登录页面
return
}
return new Promise((resolve, reject) => {
uni.request({
url: baseUrl url,
method: method,
data: data,
header: headers,
success: (res) => {
if (res.statusCode !== 200) {
uni.showModal({
title: "请求失败",
content: "接口: " baseUrl url "n" "错误码: " res.statusCode,
showCancel: false
});
console.log("request fail:", res)
reject(res)
} else {
resolve(res.data)
}
},
fail: (err) => {
uni.showModal({
title: "请求失败",
content: "接口: " baseUrl url "n" "错误码: " res.statusCode,
showCancel: false
});
console.log("request fail:", res)
reject(err)
},
complete: () => {
resolve()
return
}
});
})
}
在没有token的情况下,得到的效果如下:
5. 参考链接#
https://www.cnblogs.com/xiaofua/p/16179330.html
https://baobao555.tech/archives/40#1.header
https://jwt.io/#debugger-io