JWT鉴权详解与实战

2022-09-23 13:03:11 浏览数 (1)

1. JWT简介#

1.1 什么是JWT#

JSON Web Token (JWT) 是一个开放标准 ( RFC 7519 ),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全传输信息。此信息可以验证和信任,因为它是数字签名的。JWT 可以使用密钥(使用HMAC算法)或使用RSAECDSA的公钥/私钥对进行签名。

最常用的场景是登录授权。用户登录后,每个后续请求都将包含 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由三部分组成,分别是headerpayloadsignature

形成的形式如: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

0 人点赞