随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web Token
(JWT
)的解决方案。
JWT
是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON
对象。这种信息可以被验证和信任,因为它是数字签名的。
那么JWT
中的Token
到底是什么?接下来,我们将以登录功能为例进行Token
的分析。
登录流程
很多小伙伴对登录的流程已经很熟悉了,我们来看一个最基本的后台系统的登录流程
流程图很清楚了,接下来我们使用 V2
和 Koa
来实现一个登录过程,来看看Token
到底是什么
Vue2
+ Koa
实现登录
前端代码
1. 前端点击事件
数据的校验就忽略掉,感兴趣的同学可自行书写或者找我要源码,直接看点击事件
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
// 这里使用了VueX
this.$store
.dispatch("user/login", this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || "/" });
this.loading = false;
})
.catch(() => {
this.loading = false;
});
} else {
return false;
}
});
}
2. Vuex中的action
校验通过后触发VueX
中User
模块的Login
方法:
async login(context, userInfo) {
const users = {
username: userInfo.mobile,
password: userInfo.password
}
const token = await login(users)
// 在这里大家可以对返回的数据进行更详细的逻辑处理
context.commit('SET_TOKEN', token)
setToken(token)
}
3. 封装的接口
export function login(data) {
return request({
url: '/login',
method: 'post',
data
})
}
以上三步,是我们从前端向后端发送了请求并携带着用户名和密码,接下来,我们来看看Koa
中是如何处理前端的请求的
Koa
处理请求
首先介绍一下Koa
:
Koa
基于Node.js
平台,由Express
幕后的原班人马打造,是一款新的服务端 web 框架
Koa
的使用极其简单,感兴趣的小伙伴可以参考官方文档尝试用一下
Koa
官网:https://koa.bootcss.com/index.html#introduction
1. 技术说明
在当前案例的koa
中,使用到了jsonwebtoken
的依赖包帮助我们去加密生成和解密Token
2. 接口处理
const { login } = require("../app/controller/user")
const jwt = require("jsonwebtoken")
const SECRET = 'test_';
router.post('/login', async (ctx, next) => {
const { username, password } = ctx.request.body
// 这里是调用Controller中的login方法来跟数据库中的数据作对比,可忽略
const userList = await login(username, password)
if (!userList) {
// 这里的errorModel是自己封装的处理错误的模块
ctx.body = new errorModel('用户名或密码错误', '1001')
return
}
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓ ※ 重点看这里 ※ ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
const token = jwt.sign({ userList }, SECRET, { expiresIn: "1h" })
ctx.body = {
success: true,
state: 200,
message: 'login success',
data: token
};
return;
})
关于 JWT
上面的重点代码大家看到了,接下来具体给大家解释下JWT
Jwt
由三部分组成:header
、payload
、signature
export interface Jwt {
header: JwtHeader;
payload: JwtPayload | string;
signature: string;
}
header
头部
里面的包含的内容有很多,比如用于指定加密算法的alg
、指定加密类型的typ
,全部参数如下所示:
export interface JwtHeader {
alg: string | Algorithm;
typ?: string | undefined;
cty?: string | undefined;
crit?: Array<string | Exclude<keyof JwtHeader, 'crit'>> | undefined;
kid?: string | undefined;
jku?: string | undefined;
x5u?: string | string[] | undefined;
'x5t#S256'?: string | undefined;
x5t?: string | undefined;
x5c?: string | string[] | undefined;
}
payload
负载
payload
使我们存放信息的地方,里面包含了签发者
、过期时间
、签发时间
等信息
export interface JwtPayload {
[key: string]: any;
iss?: string | undefined;
sub?: string | undefined;
aud?: string | string[] | undefined;
exp?: number | undefined;
nbf?: number | undefined;
iat?: number | undefined;
jti?: string | undefined;
}
signature
签名
signature
需要使用编码后的header
和payload
以及我们提供的一个密钥(SECRET
),然后使用header
中指定的签名算法进行签名
关于 jwt.sign()
jwt.sign()
方法,需要三个基本参数和一个可选参数:payload
、secretOrPrivateKey
、options
和一个callback
export function sign(
payload: string | Buffer | object,
secretOrPrivateKey: Secret,
options: SignOptions,
callback: SignCallback,
): void;
payload
是我们需要加密的一些信息,这个参数对应上面koa
代码中的{ userList }
,而userList
则是我从数据库中查询得到的数据结果
secretOrPrivateKey
则是我们自己定义的秘钥,用来后续验证Token
时所用
options
选项中有很多内容,例如加密算法algorithm
、有效期expiresIn
等等
export interface SignOptions {
/**
* Signature algorithm. Could be one of these values :
* - HS256: HMAC using SHA-256 hash algorithm (default)
* - HS384: HMAC using SHA-384 hash algorithm
* - HS512: HMAC using SHA-512 hash algorithm
* - RS256: RSASSA using SHA-256 hash algorithm
* - RS384: RSASSA using SHA-384 hash algorithm
* - RS512: RSASSA using SHA-512 hash algorithm
* - ES256: ECDSA using P-256 curve and SHA-256 hash algorithm
* - ES384: ECDSA using P-384 curve and SHA-384 hash algorithm
* - ES512: ECDSA using P-521 curve and SHA-512 hash algorithm
* - none: No digital signature or MAC value included
*/
algorithm?: Algorithm | undefined;
keyid?: string | undefined;
/** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
expiresIn?: string | number | undefined;
/** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
notBefore?: string | number | undefined;
audience?: string | string[] | undefined;
subject?: string | undefined;
issuer?: string | undefined;
jwtid?: string | undefined;
mutatePayload?: boolean | undefined;
noTimestamp?: boolean | undefined;
header?: JwtHeader | undefined;
encoding?: string | undefined;
allowInsecureKeySizes?: boolean | undefined;
allowInvalidAsymmetricKeyTypes?: boolean | undefined;
}
callback
则是一个回调函数,有两个参数,默认返回Token
export type SignCallback = (
error: Error | null,
encoded: string | undefined,
) => void;
通过以上方法加密之后的结果就是一个Token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s
总结
在整个的Koa
中,用到了jsonwebtoken
这个依赖包,里面有sign()
方法
而我们前端所得到的数据则是通过sign()
所加密出来的包含自定义秘钥的一份用户信息而已
至于用户信息中有什么内容,可以随便处理,比如用户的ID、用户名、昵称、头像等等
那么这个Token
后续有什么用呢?
后续我们可以在前端的拦截器中配置这个Token
,让每一次的请求都携带这个Token
,因为Koa
后续需要对每一次请求进行Token
的验证
比如登录成功后请求用户的信息,获取动态路由,再通过前端的router.addRoutes()
将动态路由添加到路由对象中去即可