前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JWT鉴权详解与实战

JWT鉴权详解与实战

作者头像
素履coder
发布2022-09-23 13:03:11
1.5K0
发布2022-09-23 13:03:11
举报
文章被收录于专栏:素履coder素履coder

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. JWT简介#
    • 1.1 什么是JWT#
      • 1.2 JWT和session的区别#
      • 2. JWT结构#
      • 3. Go+JWT#
      • 4. uniapp+JWT#
      • 5. 参考链接#
      相关产品与服务
      消息队列 TDMQ
      消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档