简言
最近在做前端知识的复习和整理,有了一些自己新的体会。更多在于记录,通过反复的温习,写笔记消除自己以前学习知识点的误区
什么是JWT
Json Web Token, 一种基于JSON格式的认证校验Token
JWT与cookie/session认证有什么不同?
Cookie/Session认证方式
缺点:
- 需要在服务端搭建更多的额外存储服务,不是必须操作
- 如果是服务器集群模式下(负载均衡),需要每个服务器都能够读取到session,维护工程量大
- 如果在浏览器外的终端中(IOS,Android),需要手动进行Cookie本地化,程序被kill时,会清空cookie
- Cookie在浏览器端,同域下存储大小为4KB,不适用于大型复杂场景
- Cookie是明文传递的,容易引发安全问题
JWT生成Token和认证原理
JWT由三部分组成Header, Payload, Signature
- Header(用以存储JWT的元数据,例如配置加密算法)
// json配置(下面只是伪代码)
{
xxx: 'RS256', // 设置加密方式
expires: '过期时间' // 设置过期时间
....
}
// 然后使用base64UrlEncode(header json) 处理,下图有操作
- Payload(用以携带用户信息)
// json配置
{
name: 'xxx',
password: 'xxxx',
xxxxx
}
// 然后使用base64UrlEncode(payload json) 处理,下图有操作
- Signature(用以对前两部分签名,防止篡改)
过程就是首先指定一个秘钥secret,然后通过Header中指定的算法,对其进行加密
最后算出签名之后,使用Header.Payload.Signature的格式拼接起来丢给前端(这个拼接串就是
token)
这里有一个小点:base64Url和base64不同之处,=被省略,+替换成-,/替换成_,这是因为有的请求会将token放在get请求上
实践一下
在nodejs中实现JWT,需要使用jsonwebtoken 这个npm依赖
npm install jsonwebtoken
用户注册阶段
- 编写路由匹配
- REGISTER: 枚举值,匹配路由
/register - adminValidator: 校验是否存在用户名和密码
const adminValidator = async (ctx, next) => {
const { username, password } = ctx.request.body;
if (!username || !password) {
commonResult(ctx, {
message: "用户名或密码为空",
status: 500,
});
return;
}
await next();
};
- cryptPassword: 加密密码
const cryptPassword = async (ctx, next) => {
const { password } = ctx.request.body;
let salt = bcryptjs.genSaltSync(10);
// hash保存的是加密后的password
let hash = bcryptjs.hashSync(password, salt);
ctx.request.body.password = hash;
await next();
};
- verifyAdmin: 检测用户是否存在(这一步优化可以放在第二步)
const verifyAdmin = async (ctx, next) => {
const { username } = ctx.request.body;
const hasAdmin = await queryAdmin(username); // 这里执行sql
if (hasAdmin && hasAdmin.length) {
commonResult(ctx, {
message: `管路员 ${username} 已存在,请勿重复注册`,
status: 409,
});
return;
}
await next();
};
- register: 将用户名和密码写入数据库或redis
const register = async (ctx) => {
const { username, password } = ctx.request.body;
const res = await createAdmin({ username, password }); // 这里执行sql
// redis更新 - 使用ioredis库
// redis.setExpire
commonResult(ctx, res); // commonResult,封装响应body
};
登录阶段
- 编写路由匹配
- Login:同上
- adminValidator: 同上
- verifyLogin:校验用户是否存在以及密码是否正确
// 校验登陆
const verifyLogin = async (ctx, next) => {
const { username, password } = ctx.request.body;
try {
const res = await queryAdmin(username); // 这里执行sql
if (!res) {
commonResult(ctx, {
message: `用户名${username}不存在`,
status: 500,
});
return;
} else {
if (!bcryptjs.compareSync(password, res?.[0]?.password)) {
commonResult(ctx, {
message: `密码输入错误`,
status: 500,
});
return;
} else {
ctx.response.body = res;
}
}
} catch (err) {
commonResult(ctx, {
message: `用户登录失败:${err}`,
status: 500,
});
return;
}
- login:执行登录操作,将token写入cookie或是通过响应体返回token
const login = async (ctx) => {
const res = ctx.response.body;
const admin = res[0];
// 1. 获取用户信息(在token的payload中,记录id,username,admin_type)
try {
const { password, account, ...rest } = admin;
ctx.cookies.set("username", encodeURIComponent(`${account}`), {
sameSite: "strict",
});
const token = encodeURIComponent(
`${jwt.sign(rest, JWT_SECRET, { expiresIn: "1d" })}` // JWT_SECRET是服务器自己的秘钥,只有自己知道
);
ctx.cookies.set("token", token, {
expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
sameSite: "strict",
});
commonResult(ctx, {
message: `欢迎回来, ${admin.account}`,
status: 200,
});
} catch (err) {
console.log(err);
}
};
校验权限(除注册,登录以外的接口,都可以加上权限校验)
- 编写路由匹配
- GETCURRENTUSER: 同上
- verifyAuth:校验token
// 验证授权
const verifyAuth = async (ctx, next) => {
const token = ctx.cookies.get("token");
if (!token) {
commonResult(ctx, {
status: 401,
message: "登录过期,请重新登录",
});
} else {
try {
const result = jwt.verify(token, JWT_SECRET);
await next();
} catch (err) {
console.log(err); // 如果token过期或者是不合法,则会进入这里
commonResult(ctx, {
status: 401,
message: "登录过期,请重新登录",
});
}
}
};
- getCurrentUser:获取当前管理员 (这个就不写了,就是一个controller._fn了)
这样,对于一个新用户的整个JWT的授权认证过程就已完成