浅聊一下
这两天捣鼓了一下单点登录,赶紧写下来,既为记录,也为巩固...
登录
据我所知,登录有很多种方法:
- session + cookie
- 单token
- 双token
session + cookie
- 首先客户端请求登录,服务端验证账号密码
- 账号密码无误创建一个session,生成一个唯一标识符sessionID储存在session中并且返回给客户端
- 客户端会将sessionID储存在cookie中,在之后的每次请求都带上这个cookie
- 服务端验证cookie带上的sessionID,若正确则允许访问
缺点:
- 安全性堪忧,session储存在cookie中,存在安全隐患
- 服务器需要储存会话数据,随着用户登录增加,服务器压力越来越大
- 水平扩展困难,在负载均衡的情况下,如果用户被分配到不同的服务器,而会话数据没有进行跨服务器共享,那么用户可能需要重新登录
单token登录
- 用户在前端提交注册或登录请求,包含用户名和密码。
- 后端验证用户信息,生成一个包含用户身份信息和过期时间的 Token。
- 前端在后续的请求中将 Token 包含在请求头的
Authorization字段中,格式为Bearer <Token>。 - 服务器验证 Token 的签名,确保其未被篡改。检查 Token 的过期时间,确保其仍在有效期内。如果 Token 有效,服务器将允许访问受保护的资源。
优点:
- 相对于传统的session+cookie模式,token并不需要在服务端储存,解决了拓展性和依赖性的问题
缺点
- token储存在客户端,被获取以后,且token仍在有效期内,仍可以使用token访问受限内容
单点登录
终于来到了重点...既然单token因为在有效期内会被别人获取而导致安全问题,那么是不是能把token的有效时间设置的非常短,那么即使别人获取了token,也是一个过期的token,毫无用处
生成token
先来一个简单的用户界面
在客户端输入账号密码,点击登录发送给服务端,来看click方法
const login = async () => {
if (username.value.length < 5) {
alert('用户名长度不能小于5位')
return;
}
if (password.value.length < 5) {
alert('密码长度不能小于5位')
return;
}
// 发送登录请求
const { data } = await axios.post("http://localhost:3000/auth/login", {
username: username.value,
password: password.value
})
// 若请求成功,则返回一个access_token,一个refresh_token
if (data.code === 0) {
const {
access_token,
refresh_token
} = data.data
// 储存
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
// 登录状态为true
isLogin.value = true
}
}
来看看这一块的服务端是如何完成的,首先配置一下路由
module.exports = function router(app) {
return function (req, res, next) {
app.post('/auth/login', loginAction);
next();
}
}
当请求登录时,执行loginAction
exports.loginAction = (req, res) => {
const {
username,
password
} = req.body;
// 这一段是在验证账号密码
const userData = users[username];
if(!userData) {
return res.json({
code:1,
msg: '用户名不存在'
})
}
const isRightPassword = checkCryptoHash(password,userData.password);
if(!isRightPassword) {
return res.json({
code:1,
msg: '密码错误'
})
}
// 验证成功后应该获取access_token和refresh_token并且返回
const {accessToken, refreshToken} = createDoubleTokne(userData);
res.json({
code:0,
msg:'ok',
data:{
access_token:accessToken,
refresh_token:refreshToken
}
})
}
// 使用jwt创建token
function createDoubleTokne(userData) {
const accessToken = createToken(
// 用户信息
{
userId: userData.id,
username: userData.username
},
//密钥
ACCESS_TOKEN_KEY,
//配置过期时间等
ACCESS_TOKEN_OPTIONS
)
const refreshToken = createToken(
{
userId: userData.id,
username: userData.username
},
REFRESH_TOKEN_KEY,
REFRESH_TOKEN_OPTIONS
)
return {
accessToken,
refreshToken
}
}
createToken就是对jwt.sign()方法做了封装
const jwt = require('jsonwebtoken');
function createToken(secretKey, payload, options) {
return jwt.sign(secretKey, payload, options);
}
module.exports = {
createToken
}
解释一下这几个参数
-
secretKey:- 这是一个字符串或 Buffer,用于对生成的 JWT 进行签名。它类似于一个密钥,确保 JWT 的完整性和不可篡改性。只有知道这个密钥的服务器才能验证 JWT 的有效性。
-
payload:-
这是一个对象,包含需要存储在 JWT 中的用户信息或其他数据。例如:
{ userId: 1, username: 'john_doe', role: 'admin' }
-
-
options:-
这是一个对象,用于配置 JWT 的生成选项。常用的选项包括:
expiresIn:指定 JWT 的过期时间,可以是一个字符串,如'1h'(1 小时)或'7d'(7 天)。algorithm:指定签名算法,默认是'HS256'(HMAC 与 SHA-256)。
-
到了这里,我们的两个token就都生成完毕并且设置了access_token的过期时间为10秒,refresh_token的过期时间为120s,现在客户端是登录状态,向服务端请求一下数据
10s以后,access_token过期,但是refresh_token还没有过期,使用refresh_token去刷新access_token,然后用户仍然可以访问受限内容,但如果refresh_token过期了,那就得用户重新登录了,虽然refresh_token的过期时间很长,但是拿到refresh_token并不能访问受限内容
token无感刷新
上面说到,要使用refresh_token来刷新access_token,那么如何做到自动刷新呢(也就是无感刷新)
先从客户端请求受限数据,带上access_token
const loadCustomerInfo = async () => {
const { data } = await axios({
url: "http://localhost:8080/api/customer",
method: 'POST',
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`
},
data: {
userId: 1
}
})
来看服务端,服务端接收请求以后需要先验证token
module.exports = function(app) {
return function(req,res,next) {
app.post('/api/customer',verifyToken(ACCESS_TOKEN_KEY),getCustomer);
next();
}
}
先看验证token
const jwt = require('jsonwebtoken')
module.exports = function verifyToken(secretKey) {
/**
* 前端发送请求的时候携带token的地方和格式
* 1. headers => authorization: Bearer token
*/
return function (req, res, next) {
// 获取到access_token
const token = req.headers.authorization.split(' ')[1];
// 使用jwt.verify来验证token
jwt.verify(token, secretKey, (err, decoded) => {
// 验证失败返回token无效
if(err) {
console.log(err);
return res.json({
code:2,
msg: 'token无效'
})
}
// 成功则返回decoded 里面包含了用户信息
req.tokenInfo = decoded;
next();
})
}
}
验证成功则返回数据,我们来看验证失败的情况
const loadCustomerInfo = async () => {
const { data } = await axios({
url: "http://localhost:8080/api/customer",
method: 'POST',
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`
},
data: {
userId: 1
}
})
//验证成功返回code=0
if (data.code === 0) {
customerInfo.value = data.data
}
//验证失败返回2 需要用refresh_token刷新access_token
if (data.code === 2) {
refreshToken((data) => {
if (data.code === 2) {
isLogin.value = false
return;
}
const {
access_token,
refresh_token
} = data.data
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
loadCustomerInfo();
})
}
}
来看一下refreshToken
async function refreshToken(callback) {
// 携带refresh_token向服务端请求刷新accsess_token
const { data } = await axios({
url: "http://localhost:3000/auth/refresh",
method: 'POST',
headers:{
authorization: `Bearer ${localStorage.getItem('refresh_token')}`
}
})
//将返回的data作为参数,调用callback函数
callback(data);
}
服务端是如何写的refresh请求呢?
module.exports = function router(app) {
return function (req, res, next) {
app.post('/auth/login', loginAction);
//还是先校验refresh_token,校验完调用refreshToken
app.post('/auth/refresh', verifyToken(REFRESH_TOKEN_KEY), refreshToken);
next();
}
}
exports.refreshToken = (req, res) => {
// 记得刚才验证token的时候将用户信息存在tokenInfo中吗
const {tokenInfo} = req;
const {userId, username} = tokenInfo;
// 校验用户信息
const userData = users[username];
// 校验成功则重新生成两个token,并且返回给客户端
if(userData && userData.id === userId) {
const {
accessToken,
refreshToken
} = createDoubleTokne(userData);
return res.json({
code:0,
msg:'ok',
data:{
access_token:accessToken,
refresh_token:refreshToken
}
})
}
// 校验失败则说明需要重新登录了
res.json({
code:1,
msg:'需要重新登录'
})
}
再重新回到客户端,我们已经获取到了新的token
if (data.code === 2) {
refreshToken((data) => {
// 如果失败则说明需要重新登录
if (data.code === 2) {
isLogin.value = false
return;
}
// 成功了则重新储存access_token和refresh_token
const {
access_token,
refresh_token
} = data.data
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
//记得需要重新调用方法获取受限资源
loadCustomerInfo();
})
}
到这里单点登录算是完成了...
总结
不知道大家对登录还有什么其他的idea...