JsonWebToken
官方文档:jsonwebtoken - npm
我们使用第三方来创建以及校验 token
jsonwebtoken
生成与校验有同步和异步方法,具体是是否传入 callback
函数来确认的
关于 token
- 用户在前端输入
username
与password
,并点击回车 - (用户浏览器中运行的)前端把
username
与password
发送至 API 中指定的 URL(使用tokenUrl="token"
声明) - API 检查
username
与password
,并用令牌(Token
) 响应 - 令牌只是用于验证用户的字符串
- 一般来说,令牌会在一段时间后过期
- 过时后,用户要再次登录
- 这样一来,就算令牌被人窃取,风险也较低。因为它与永久密钥不同,在绝大多数情况下不会长期有效
- 前端临时将令牌存储在某个位置
- 用户点击前端,前往前端应用的其它部件
- 前端需要从 API 中提取更多数据:
- 为指定的端点(Endpoint)进行身份验证
- 因此,用 API 验证身份时,要发送值为
Bearer
+ 令牌的请求头Authorization
- 假如令牌为
foobar
,Authorization
请求头就是:Bearer foobar
生成
前置数据
payload
:需要保存的数据确保传入的是 对象 或 json 数据,其他数据会报错。记住,不要存密码!
secretKey
:秘钥,用于加密解密bash# 可通过以下方式来生成,唯一 openssl rand -hex 32
1
2expiresIn
:过期时间,默认单位(秒)- 可以使用字符串:
2 days
,2d
,10h
,10m
,10s
来表示 - 也可以直接计算:例如
(60 * 5)
代表 5 分钟
- 可以使用字符串:
typescript
const payload = {name: '张三', age: 12}
const secretKey = 'abc123'
const expiresIn = '2d'
1
2
3
2
3
生成
typescript
import * as jwt from "jsonwebtoken"
jwt.sign(payload, secretKey, {
expiresIn: expiresIn
}, async (error, encoded) => {
console.log(encoded)
})
1
2
3
4
5
6
7
2
3
4
5
6
7
typescript
import * as jwt from "jsonwebtoken"
const token = jwt.sign(payload, secretKey, {expiresIn: expiresIn})
console.log(token)
1
2
3
4
2
3
4
过期时间
两种写入方式,
- 第一种是,保存到
payload
中 推荐 - 第二种是,生成时传入
jwt.sign
的option.expiresIn
中
为什么推荐第一种?
可以生成到期时间戳,将这个到期时间戳传给前端
前端通过判断到期时间戳来判断是否跳转到登录页
在之前的开发中,每次用户在操作提交(post
,put
等请求)时才判断token
是否过期,现在如果前端加一层判断,每次用户打开页面或发送get
请求时就判断是否过期,
typescript
import * as jwt from "jsonwebtoken"
const expTime = Math.floor(Date.now() / 1000) + 20, // 20秒后过期
jwt.sign({
exp:expTime,
data: data, // 存储的数据
}, secretKey, {}, async (error, encoded) => {
console.log(encoded)
})
/*
注意异步回调函数是第四个参数,第三个参数为:option,
这里不需要,option,所以第三个参数传入空对象
如果不传,会报错
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript
jwt.sign(payload, secretKey, {
expiresIn: expiresIn
}, async (error, encoded) => {
console.log(encoded)
})
1
2
3
4
5
2
3
4
5
校验
验证成功,则返回数据
时间过期,报错:
TokenExpiredError: jwt expired
验证失败,报错:
JsonWebTokenError: invalid token
警告
TokenExpiredError
继承于JsonWebTokenError
所以,在
catch
中应该将时间过期写在上面,不然直接捕捉的是JsonWebTokenError
验证 token
也需要 secretKey
,并且要与生成 token
时使用的一致
typescript
import * as jwt from "jsonwebtoken"
import {JsonWebTokenError, TokenExpiredError} from "jsonwebtoken";
const secretKey = 'abc123'
const token = '此处省略'
jwt.verify(token, secretKey, {}, async (error, decoded) => {
if (error) {
if (error instanceof TokenExpiredError) {
throw new Error('token 时间过期')
} else if (error instanceof JsonWebTokenError) {
throw new Error('Token 非法')
} else {
throw new Error('其他错误')
}
}
console.log(decoded)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript
try {
const decoded = jwt.verify(token, secretKey)
console.log(Math.floor(Date.now() / 1000))
console.log(decoded)
} catch (e) {
if (e instanceof TokenExpiredError) {
throw new Error('token 过期')
} else if (e instanceof JsonWebTokenError) {
throw new Error('token 不合法')
} else {
throw new Error('其他错误')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
实例
结合 express
封装的 token
解析
示例
typescript
import {sign, verify} from 'jsonwebtoken'
import {NextFunction, Request, Response} from "express";
// 解析出来的 token 返回这种数据
interface JwtData {
exp?: number
iot?: number
data: object
}
// 扩展 Express 的 Request 接口
declare global {
namespace Express {
interface Request {
user: { [key: string]: any }; // 定义 locals 属性的类型
}
}
}
// 设置过期时间,为时间戳
const expTime = Math.floor(Date.now() / 1000) + (60 * 60)
/**
* 创建 token
* @param data 需要 sign 的数据
*/
export const createToken = (data: Object): Promise<string | undefined | Error | null> => {
return new Promise((resolve, reject) => {
sign({
exp: expTime,
data,
},
'SECRETKEYKEYKEYKEY',
{},
async (error, encoded) => {
if (error) reject(error)
resolve(encoded)
})
})
}
/**
* 解析 token
* @param token
*/
const verifyToken = (token: string) => {
return new Promise((resolve, reject) => {
verify(token, 'SECRETKEYKEYKEYKEY', async (error, decoded) => {
if (error) reject(error)
// const data = decoded as JwtData
resolve(decoded)
})
})
}
/**
* 校验 token 中间件
*/
export const checkToken = async (req: Request, res: Response, next: NextFunction) => {
const token = req.header('Authorization')
// 校验是否有 Authorization,以及是否为 'Bearer '
if (!token || token.slice(0, 7) !== 'Bearer ') {
res.status(401).jsonp({
err: 4000,
message: '请登录!'
})
} else {
try {
// 将 Bearer 删除,传入解析 token 函数
const user = await verifyToken(token.slice(7)) as JwtData
req.user = user.data
next()
} catch (e) {
res.status(401).jsonp({
err: 4001,
message: '授权失败!'
})
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84