开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情
你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹
现在有一个需求,我在网站登录过一次之后,下一次打开直接是登录状态,无需再次输入账号密码。
接下来我会用两种方法来实现:Token,Session。要实现自动登录,首先想到的是自动发送账号密码登录,这样的话前端需要存储账号密码,无非就是cookie,localstorage,sessionstorage等等这些存储方法。但是这些都是可见的,账号密码非常容易泄露。因此需要一个不会暴露账号密码的方法,而且可以记录用户的状态。
这篇文章会涉及cookie、token、session这些知识点,不熟悉的同学可以看Cookie、Session、token小白科普版这篇文章了解一下每一个的工作原理。
以下例子适用于前后端分离的项目,本文前端采用React,服务端是express
服务端搭建
const express = require('express')
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.listen(80, () => {
console.log("express running at port 80...");
})
Session实现自动登录
工作原理:用户登录,服务端通过cookie将sessionID发送给浏览器,由于cookie设置了httpOnly,所以document.cookie取不到cookie。当登录成功在localstorage存一个登录标识。遇到/和/login这两个路由时,判断标识是否存在,存在就直接跳过登录页。如果session无效或者过期,服务端返回错误状态码10101。前端做请求拦截,遇到10101报错,跳转登录页。
服务端代码
const sessions = require('express-session')
// 模拟数据库中的用户名和密码
const username_DB = 'Nanyi'
const password_DB = '123'
// 存储产生的session
let session_DB
app.use(sessions({
secret: '密钥',
name: 'sessionName',
cookie: { maxAge: 150000, expires: new Date('2024-1-10') },
resave: false,
saveUninitialized: false
}))
// 请求拦截,如果session里没有name字段,证明session失效,需要重新登录
app.use(function (req, res, next) {
if (req.originalUrl === '/login' || req.session.name !== undefined) {
next()
} else {
res.send({
code: 10101,
status: 'error',
message: '登录状态已过期,请重新登录'
})
res.end()
}
})
// 登录时,验证用户信息是否在数据库中,将session的信息保存到数据库,往session加入name字段
app.post('/login', function (req, res) {
if (req.body.name === username_DB && req.body.password === password_DB) {
session_DB = req.session
session_DB.name = req.body.name
res.send({
code: 200,
status: 'success',
message: '登录成功'
})
} else {
res.send({
code: 400,
status: 'error',
message: '用户名或密码错误'
})
}
})
app.get('/safe', function (req, res) {
res.send({
code: 200,
status: 'success',
message: `欢迎登录${session_DB.name}`
})
})
前端代码
在web应用挂载的时候,开启全局响应拦截,并将navigate函数作为参数传递过去,用于路由跳转。如果报错10101,则跳转登录页并删除登录标识。
export const interceptors = function (navigate) {
axios.interceptors.response.use(function (res) {
if (res.data.code === 10101) {
localStorage.removeItem('islogin')
navigate('/login')
}
return res
})
}
// App
export default function App(){
const navigate = useNavigate()
useLayoutEffect(() => {
interceptors(navigate)
}, [])
return (...)
}
登录页,如果有登录标识就跳转到内部主页;点击登录时,将登录标识存在loaclstorage,并跳转内部主页
export default function Login() {
const navigate = useNavigate()
useEffect(() => {
let islogin = localStorage.getItem('islogin')
islogin && navigate('/CSRF')
}, [])
const login = () => {
axios.post('/login', { name: 'Nanyi', password: '123' }).then((res) => {
alert(res.data?.message)
localStorage.setItem('islogin', 'true')
navigate('/CSRF')
})
}
return (...)
}
Token实现自动登录
工作原理:用户登录,服务端返回token,前端将token存在localstorage。遇到/和/login这两个路由时,判断标识是否存在,存在就直接跳过登录页。如果token无效或者过期,服务端返回错误,状态码10101(用于标识是token错误),需要重新登录。前端做请求拦截,遇到10101报错,跳转登录页。
服务端代码
const jwt = require('jsonwebtoken')
let JWT_SECRET = '密钥'
let user_DB // 存token里面解析出来的用户信息
app.use(function (req, res, next) {
const { authorization } = req.headers
const token = authorization.replace('Bearer ', '')
try {
user_DB = jwt.verify(token, JWT_SECRET)
} catch (error) {
let message = ''
switch (error.name) {
case 'TokenExpiredError':
message = 'token已过期'
break;
case 'JsonWebTokenError':
message = '无效token'
break;
}
res.send({
code: 10101,
status: 'error',
message: message
})
res.end()
return;
}
next()
})
app.post('/login', function (req, res) {
let name = req.body.name
res.send({
code: 200,
status: 'success',
message: {
token: jwt.sign({ name }, JWT_SECRET, { expiresIn: '1d' })
}
})
})
app.get('/safe', function (req, res) {
res.send({
code: 200,
status: 'success',
message: `欢迎登录${user_DB.name}`
})
})
前端代码
在web应用挂载的时候,开启全局响应拦截,并将navigate函数作为参数传递过去,用于路由跳转。如果报错10101,则跳转登录页并删除token。
export const interceptors = function (navigate) {
axios.interceptors.response.use(function (res) {
if (res.data.code === 10101) {
localStorage.removeItem('islogin')
localStorage.removeItem('token')
navigate('/login')
}
return res
})
}
// 登录时,把token存在localstorage
const login = () => {
axios.post('/login', { name: 'Nanyi', password: '123' }).then((res) => {
alert(res.data?.message)
localStorage.setItem('islogin', 'true')
localStorage.setItem('token', res.data?.token)
navigate('/CSRF')
})
}
前端代码与session的实现基本一致,只需要在以上两处加上一句代码,在localstorage存删token即可。由于token是放在请求头传递,所以需要做一步请求拦截。
axios.interceptors.request.use(function (config) {
let token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = 'Bearer ' + token
}
return config;
});
总结
至此,我们实现了自动登录,以及用户身份过期跳回登录页。自动登录前端部分的实现大同小异,根据服务端身份验证的实现进行调整。