当开发全栈项目时,鉴权功能是必不可少的一部分,那前端如何利用node实现鉴权呢?
- Session/Cookie
- Token
- OAuth
- SSO
下面主要针对前两种来做介绍。如有描述不对的地方,欢迎指正。
1. Session/Cookie
1.1 cookie
实现方式:
- 客户端登录页完成首次登录后,调用服务端登录接口。
- 服务端校验完成后,在响应头设置cookie内容。
- 客户端接收到请求响应中服务端设置的cookie内容后,会存在浏览器本地。
- 下次客户端再次请求时,请求头会自动携带浏览器本地的cookie给服务端。
服务端设置cookie字段:
// 设置cookie
res.setHeader('Set-Cookie', 'cookie1=abctest')
cookie主要缺点:
- 浏览器cookie有长度限制,所存内容有限
- 安全性不高,内容都是明文传输
1.2 session/cookie
session会话机制是一种服务器端机制,使用类似于哈希表的结构来保存信息。一般cookie会与session结合使用,cookie只保存sessionId,服务端通过 sessionId 来获取具体的值(value信息)。也就是说具体值(value)会保存在服务端,sessionId会随cookie返回并保存在客户端。
概览图:
实现思路:
- 客户端登录页完成首次登录后,调用服务端登录接口。
- 服务端接收请求后,创建 session 并保存 session, session可保存在内存中(不建议),也可保存在指定的缓存服务器中,比如redis(键值对服务器)。然后根据session 生成唯一标识符 sessionId, 并在响应头的cookie中设置这个唯一sessionId。也可以选择通过密钥对sessionId进行签名,避免sessionId被篡改。
- 客户端收到请求响应后,请求头中的cookie信息会被缓存在浏览器本地,其中包括cookie中设置的sessionId等信息。
- 客户端下次发起请求时,http的请求头会携带这个包含sessionId的cookie信息。
- 服务端接收到请求后,解析请求头中携带的cookie,获取sessionId值,并根据sessionId去数据库或内存中查找对应的session值,然后判断值是否合法。
一般可以考虑使用redis(键值对服务器)作为缓存服务器,来保存session的值。
代码示例:
const Koa = require('koa')
const router = require('koa-router')()
const session = require('koa-session')
const redisStore = require('koa-redis')({
port: 6379, // Redis port
host: "127.0.0.1", // Redis host
password: "123456",
});
const cors = require('koa2-cors')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const app = new Koa();
app.keys = ['secret']; // 密钥
app.use(cors({
credentials: true
}))
app.use(static(__dirname + '/'));
app.use(bodyParser())
app.use(session({
key: 'sessionId',
store: redisStore
}, app));
app.use((ctx, next) => {
if (ctx.url.indexOf('users') > -1) {
// 如果是用户信息相关接口
next()
} else if (ctx.session.userinfo){
// 如果已经有了用户信息
next()
} else {
ctx.body = {
message: "暂无登录信息"
}
}
})
// 登录
router.post('/users/login', async ctx => {
const { body } = ctx.request
// body中读取用户名和密码信息
// 用户名和密码与数据库值做比对
// ...
// ...
// 比对成功后设置session
ctx.session.userinfo = body.username;
ctx.body = {
message: "账号登录成功"
}
})
// 登出
router.post('/users/logout', async ctx => {
// 删除session
delete ctx.session.userinfo
ctx.body = {
message: "账号登出成功"
}
})
// 获取用户信息
router.get('/users/getUser', async ctx => {
if(ctx.session.userinfo){
ctx.body = {
message: "用户信息获取成功",
userinfo: ctx.session.userinfo
}
}else{
ctx.body = {
message: "用户信息获取失败"
}
}
})
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
访问以上请求,能观察到请求cookie中携带的sessionId, sessionId.sig字段信息。
方案缺点:
- 客户端需要支持session, cookie,平台受限
- session需要额外维护
2. token
使用token令牌验证应该是项目中比较常用的鉴权方式。利用jsonwebtoken插件生成JWT令牌。
概览图:
实现思路:
- 首次访问时,客户端携带用户名等信息调用登录接口。
- 服务端接收到请求后,请求体中取出用户信息,与数据库中信息做校验比对。
- 比对成功后,服务端使用JWT插件,利用密钥生成JWT令牌。
- 服务端将JWT令牌信息设置到响应头中。
- 客户端获取响应后,响应头中取出JWT并存储,比如localStorage里。
- 客户端再次请求时,在请求头中设置存储过的JWT令牌信息。
- 服务端获取请求后,在请求头中取出JWT,检查JWT签名是否合法,利用密钥解密获取用户信息。
- 服务端将用户信息返回。
代码示例:
前台页面登录完成后,需要将返回的token设置到请求头中,主要代码如下:
// Bearer是JWT的认证头部信息
config.headers.common["Authorization"] = `Bearer ${token}`;
后端核心代码如下:
const koa = require('koa')
const router = require('koa-router')()
const jwt = require('jsonwebtoken')
const jwtAuth = require('koa-jwt')
const bodyParser = require('koa-bodyparser')
const secret = 'secret' // 鉴权的密钥
const app = new koa()
app.keys = ['secret']
app.use(bodyParser())
// 登录
router.post('/user/login', async ctx => {
const { body: { username, password, role } } = ctx.request
// body中读取用户名和密码信息
// 用户名和密码与数据库值做比对
// ...
// ...
// 比对成功后生成token令牌
const userinfo = {
username,
password,
role
}
const token = jwt.sign({
data: userinfo,
exp: Math.floor(Date.now()/1000) + 60 * // 设置token有效期
}, secret)
// 返回响应
ctx.body = {
message: '登录成功',
userinfo,
token
}
})
// 获取token
router.get('/user/token', jwtAuth({
secret
}), async ctx => {
ctx.body = {
message: '获取成功',
userinfo: ctx.state.user.data // ctx.state返回值见下面
}
}
)
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)
// ctx.state返回值
{
"user": {
"data": {
"username": "admin",
"password": "123456",
"role": "12"
},
"exp": 1639998728,
"iat": 1639995128
}
}
示例token如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsInJvbGUiOiIxMiJ9LCJleHAiOjE2Mzk5OTkzMzIsImlhdCI6MTYzOTk5NTczMn0.60C30yGV-eAae6dHzU8zRrRfwtowfmwzkKjd0rndGaQ
3. JWT(JsonWebToken)
上面提到的token令牌是使用了JWT对内容编码生成的。下面简单介绍下:
3.1 token格式
Bearer token包含三部分:令牌头,payload,哈希,以"."分隔。
token示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsInJvbGUiOiIxMiJ9LCJleHAiOjE2Mzk5OTkzMzIsImlhdCI6MTYzOTk5NTczMn0.60C30yGV-eAae6dHzU8zRrRfwtowfmwzkKjd0rndGaQ
令牌头:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload: eyJkYXRhIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsInJvbGUiOiIxMiJ9LCJleHAiOjE2Mzk5OTkzMzIsImlhdCI6MTYzOTk5NTczMn0
哈希:60C30yGV-eAae6dHzU8zRrRfwtowfmwzkKjd0rndGaQ
令牌头
使用base64编码,解密后格式:
// atob(string) 解码使用 base64 编码的字符串。
// btoa(string) 使用base64编码。
JSON.parse(window.atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'))
// 解密后数据
{
alg: 'HS256', // 使用HS256算法加密生成hash
typ: 'JWT' // 指定类型为JWT
}
payload
默认使用base64编码,解密后格式:
JSON.parse(window.atob('eyJkYXRhIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsInJvbGUiOiIxMiJ9LCJleHAiOjE2Mzk5OTkzMzIsImlhdCI6MTYzOTk5NTczMn0'))
// 解密后数据
{
data: {
username: "admin",
password: "123456",
role: "12"
},
exp: 1639999332,
iat: 1639995732
}
哈希
签名:使用HS256算法对令牌头,payload和密钥进行综合签名生成哈希。内容非明文,未知。
3.2 token内容
token签名
调用sign方法可以生成签名,代码如下:
// 生成签名
const jwt = require('jsonwebtoken')
const secret = 'lalalala' // 密钥
let token = jwt.sign({
data: userinfo,
exp: Math.floor(Date.now()/1000) + 60 *60 // 设置token的有效期
}, secret)
token解码
调用verify或者decode方法可以解密token,代码如下:
// 签名解码
const jwt = require('jsonwebtoken')
const secret = 'lalalala' // 密钥
let result = jwt.verify(token, secret, {
secret: 'jwt_secret', // 指定jwt加密方式
})
token失效
token签名中可以设置过期时间字段,比如exp。token解密后获取过期时间,然后判断当前时间是否大于过期时间,如果大于说明token已经失效了,这时候走token失效逻辑。
以上内容来自网络课程总结。